<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>짧은코딩</title>
    <link>https://shortcoding.tistory.com/</link>
    <description>전공 수업 정리 및 개인 코딩과 프로젝트 올리는 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 11 May 2026 01:05:08 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>5_hyun</managingEditor>
    <image>
      <title>짧은코딩</title>
      <url>https://tistory1.daumcdn.net/tistory/4949639/attach/2e44e65565594c74b5c11e1583f4a7ec</url>
      <link>https://shortcoding.tistory.com</link>
    </image>
    <item>
      <title>Tailwind CSS 라이브러리 Zero-Config 구축 전략</title>
      <link>https://shortcoding.tistory.com/586</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;자주 사용하는 UI를 라이브러리화 하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Material-UI(MUI)에서 Tailwind CSS 기반으로 마이그레이션 하는 과정이 있었는데, 이 라이브러리를 사용하는 소비 프로젝트의 설정 복잡도를 &quot;Zero&quot;로 만드는 과정을 기록하고자 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 도입 배경 및 문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 Tailwind CSS 기반 라이브러리 배포 방식은 사용자에게 과도한 설정을 요구하며, 이는 개발 경험(DX) 저하&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1 기존 방식의 한계&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;소스 코드 제공 방식&lt;/b&gt;: 사용자가&amp;nbsp;&lt;b&gt;tailwind.config.js&lt;/b&gt;의&amp;nbsp;content&amp;nbsp;배열에 라이브러리 경로를 직접 추가해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CSS 파일 번들링 방식&lt;/b&gt;: 빌드된 CSS 파일을 제공하고 사용자가 이를 import 해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2 결정적 문제점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설정 복잡도 증가&lt;/b&gt;:&amp;nbsp;&lt;b&gt;tailwind.config.js&lt;/b&gt;&amp;nbsp;수정 누락 시 스타일이 적용되지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Yarn PnP 호환성 문제&lt;/b&gt;: Yarn Berry(PnP) 환경에서는&amp;nbsp;node_modules가 존재하지 않으므로, 라이브러리 내부 CSS 경로를 참조하기 위해 사용자가&amp;nbsp;&lt;b&gt;yarn unplug 라이브러리명&lt;/b&gt;&amp;nbsp;명령어를 실행하여 강제로 압축을 해제해야 하는 치명적인 불편함이 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 핵심 솔루션: CSS Injection (JS-in-CSS)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해, CSS를 별도 파일로 분리하지 않고 &lt;b&gt;자바스크립트 번들에 내장(Injection)&lt;/b&gt;하는 전략을 채택&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1 동작 원리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 아키텍처는&amp;nbsp;&lt;b&gt;Build Time(라이브러리 빌드 시점)&lt;/b&gt;과&amp;nbsp;&lt;b&gt;Run Time(소비 프로젝트 실행 시점)&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;Build Time(라이브러리 빌드 시점)&lt;/b&gt;&lt;br /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Rollup 번들러가 진입점(Entry)을 통해 의존성 그래프를 그린다.&lt;/li&gt;
&lt;li&gt;컴포넌트에서 import 된 CSS 파일을 PostCSS 플러그인이 가로챈다.&lt;/li&gt;
&lt;li&gt;Tailwind 컴파일러가 실행되어 Utility Class들을 실제 CSS 문자열로 변환한다.&lt;/li&gt;
&lt;li&gt;생성된 CSS 문자열은&amp;nbsp;&lt;b&gt;.css&lt;/b&gt;&amp;nbsp;파일로 추출되는 대신, 자바스크립트 변수에 할당되고&amp;nbsp;styleInject라는 헬퍼 함수와 함께 번들링 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Run Time(소비 프로젝트 실행 시점)&lt;/b&gt;&lt;br /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가&amp;nbsp;import { AlertMessage }... 를 실행하여 라이브러리를 로드한다.&lt;/li&gt;
&lt;li&gt;번들 내부의 코드가 실행되면서&amp;nbsp;styleInject&amp;nbsp;함수가 호출된다.&lt;/li&gt;
&lt;li&gt;이 함수는 동적으로&amp;nbsp;&amp;lt;style&amp;gt;&amp;nbsp;태그를 생성하고, 내장된 CSS 문자열을 주입하여&amp;nbsp;document.head에 append 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2 구현 순서(UI 라이브러리에서 구현하는 순서)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Step 1: Tailwind 환경 구성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리 내부에서 스타일을 정의하기 위해 표준적인 Tailwind 환경을 구성한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;/* src/style.css */
@tailwindbase;
@tailwindcomponents;
@tailwindutilities;

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Step 2: 의존성 주입&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 파일이 스타일 파일을 명시적으로 의존하도록 import 구문을 추가한다. 이는 번들러가 스타일 파일을 처리하도록 트리거하는 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/AlertMessage.tsx
import'./style.css';

export function AlertMessage(){ ... }

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Step 3: Rollup 빌드 파이프라인 구성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rollup.config.js에서&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;rollup-plugin-postcss
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 설정하여 CSS Injection을 활성화한다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;// rollup.config.js
importpostcssfrom'rollup-plugin-postcss';
importtailwindcssfrom'tailwindcss';
importautoprefixerfrom'autoprefixer';

exportdefault{
// ...
plugins:[
postcss({
inject:true,// 핵심: CSS를 JS에 주입
minimize:true,// CSS 압축
plugins:[tailwindcss(),autoprefixer()],
}),
// ...
],
};

&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;&lt;b&gt;3. 기술적 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1 장점&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Zero Configuration&lt;/b&gt;: 사용자는 라이브러리 설치 외에 어떠한 추가 설정(Webpack, Tailwind Config 등)도 필요 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;완벽한 캡슐화&lt;/b&gt;: 스타일이 라이브러리 내부에 격리되어 배포되므로, 외부 환경(어떤 패키지 매니저를 사용하던 상관없음, 경로 별칭 등)에 영향을 받지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DX(개발자 경험) 향상&lt;/b&gt;:&amp;nbsp;import&amp;nbsp;실수로 인한 스타일 누락 이슈를 원천 차단한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Yarn PnP 호환성&lt;/b&gt;: 파일 시스템 경로에 의존하지 않으므로,&amp;nbsp;unplug&amp;nbsp;과정 없이 Zip 아카이브 상태에서도 정상 동작한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2 단점&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;번들 사이즈 증가&lt;/b&gt;: CSS가 자바스크립트 문자열로 인코딩 되어 포함되므로, JS 번들 크기가 증가한다.&amp;nbsp;&lt;b&gt;(본 프로젝트 기준: 120KB &amp;rarr; 127KB, 약 7KB 증가)&lt;/b&gt;. 이는 초기 파싱 속도에 미세한 영향을 줄 수 있으나, 별도의 CSS 파일을 로드하는 네트워크 요청이 제거되므로 전체 로딩 시간에는 큰 영향이 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커스터마이징 제약&lt;/b&gt;: 빌드 시점에 CSS가 확정되므로, 소비 프로젝트의&amp;nbsp;&lt;b&gt;tailwind.config.js&lt;/b&gt;&amp;nbsp;테마 설정을 라이브러리 내부 컴포넌트에 동적으로 반영할 수 없다. (단,&amp;nbsp;className&amp;nbsp;prop을 통한 오버라이딩은 가능)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CSS-in-JS 런타임 오버헤드&lt;/b&gt;: 스타일 태그를 동적으로 생성하는 아주 짧은 스크립트 실행 시간이 필요하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 운영 이슈: Nexus 패키지 불변성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리 배포 운영 중, 변경 사항을 반영하기 위해 동일 버전(1.1.1)을 삭제 후 재배포하였으나 소비 프로젝트에 반영되지 않는 현상이 관측되었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.1 원인: 불변성 원칙 위배&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 패키지 매니저(Yarn Berry 등)와 저장소(Nexus)는 패키지의 &lt;b&gt;불변성&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;Checksum Verification&lt;/b&gt;:&amp;nbsp;&lt;b&gt;yarn.lock&lt;/b&gt;&amp;nbsp;파일은 설치된 패키지의 무결성 해시를 저장한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Caching Layer&lt;/b&gt;: 로컬 캐시 및 Nexus 캐시 레이어는 '버전'을 키(Key)로 리소스를 저장하며, 동일 버전에 대한 갱신 요청을 무시하는 경향이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.2 해결 방안&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&quot;배포된 패키지는 수정되지 않는다&quot;&lt;/b&gt;는 원칙을 준수해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Semantic Versioning&lt;/b&gt;: 수정 사항이 발생하면 반드시 버전을 올린다 (patch&amp;nbsp;버전 증가).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Snapshot 배포&lt;/b&gt;: 테스트가 필요한 경우 정식 버전이 아닌&amp;nbsp;1.1.2-alpha.1과 같은 Pre-release 태그를 사용한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 결론 (Conclusion)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 라이브러리와 같이 '사용 편의성'이 중요한 프로젝트에서&amp;nbsp;&lt;b&gt;CSS Injection&lt;/b&gt;&amp;nbsp;전략은 매우 강력한 도구이다. 비록 번들 사이즈 측면의 트레이드오프가 존재하나, 설정 복잡도를 제거함으로써 얻는 생산성 향상과 유지보수 이점이 훨씬 크다고 판단된다. 또한, 안정적인 배포를 위해서는 패키지 버전 관리 원칙을 엄격히 준수해야 함을 확인하였다.&lt;/p&gt;</description>
      <category>개발</category>
      <category>nexus</category>
      <category>tailwind</category>
      <category>라이브러리</category>
      <author>5_hyun</author>
      <guid isPermaLink="true">https://shortcoding.tistory.com/586</guid>
      <comments>https://shortcoding.tistory.com/586#entry586comment</comments>
      <pubDate>Wed, 7 Jan 2026 22:28:19 +0900</pubDate>
    </item>
    <item>
      <title>React Server Components 독후감</title>
      <link>https://shortcoding.tistory.com/585</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;RSC(React Server Components)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763478558431&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;rfcs/text/0188-server-components.md at main &amp;middot; reactjs/rfcs&quot; data-og-description=&quot;RFCs for changes to React. Contribute to reactjs/rfcs development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md&quot; data-og-url=&quot;https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;rfcs/text/0188-server-components.md at main &amp;middot; reactjs/rfcs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;RFCs for changes to React. Contribute to reactjs/rfcs development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RSC가 탄생하게 된 문서를 읽어보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RSC가 필요했던 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSR은 UX와 성능 사이에서 트래이드오프가 항상 존재했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포넌트가 렌더링 되고 데이터를 요청하는 패턴(useEffect)가 반복되면, 데이터 로딩이 지연된다&lt;/li&gt;
&lt;li&gt;npm 라이브러리를 계속 설치하다보면 번들 사이즈가 커지게된다.&lt;/li&gt;
&lt;li&gt;성능을 위해 데이터를 최상위에서 한번에 가져오면, 하위 컴포넌트의 데이터 의존성이 상위 컴포넌트에 강하게 결합되어 유지보수가 힘들어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결책&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;컴포넌트 트리의 일부는 서버에서 실행하고, 일부는 브라우저에서 실행하자&quot;가 해결책이었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Server Component: 데이터 로딩, DB 접근, 무거운 연산 담당(브라우저로 코드 전송 X)&lt;/li&gt;
&lt;li&gt;Client Component: 클릭, 입력, 상태 관리 같은 인터랙션 담당(브라우저로 코드 전송 O)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RSC 동작 원리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;직렬화와 제약 사항
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직렬화는 문자열 데이터 변환이 가능한 데이터를 의미한다.&lt;/li&gt;
&lt;li&gt;따라서 JSON, JSX Element는 가능하다.&lt;/li&gt;
&lt;li&gt;하지만 함수, 클래스 인스턴스, DOM API는 불가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;HTML이 아닌 '데이터 스트림' 전송&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RSC는 HTML을 주고 받지 않는다. HTML을 주고 받는 것은 SSR이다&lt;/li&gt;
&lt;li&gt;RSC는 JSON과 유사한 데이터 스트림을 보낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764078259499&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 개념적 예시 (실제 프로토콜은 더 복잡함)
{
  &quot;id&quot;: &quot;root&quot;,
  &quot;children&quot;: [
    { &quot;type&quot;: &quot;div&quot;, &quot;children&quot;: &quot;서버에서 렌더링된 텍스트&quot; },
    { &quot;type&quot;: &quot;ClientComponent&quot;, &quot;props&quot;: { &quot;data&quot;: &quot;...&quot; }, &quot;path&quot;: &quot;chunk.js&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;3. 상태 유지&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 스트림 방식으로 인해 얻는 장점이다.&lt;/li&gt;
&lt;li&gt;HTML을 받아오는 것이 아니기 때문에 새로운 데이터를 서버에서 받아와도 전체를 수정하지 않고, 수정이 필요한 컴포넌트만 머지해서 클라이언트 컴포넌트가 갖고 있던(input 포커스, 스크롤 위치, 입력 중인 텍스트 등)은 유지된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-1번 API 없이 DB 조회&lt;/p&gt;
&lt;pre id=&quot;code_1764078909789&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app/users/page.tsx (기본적으로 Server Component)
import { db } from '@/lib/db'; // Prisma나 ORM 가정

// 1. 컴포넌트 자체가 async 함수입니다.
export default async function UsersPage() {
  // 2. 별도의 API 호출 없이 바로 DB를 찌릅니다.
  // 이 코드는 서버에서만 돌기 때문에 DB URL 등이 브라우저에 노출되지 않습니다.
  const users = await db.user.findMany({
    where: { isActive: true },
    orderBy: { createdAt: 'desc' }
  });

  return (
    &amp;lt;main className=&quot;p-10&quot;&amp;gt;
      &amp;lt;h1 className=&quot;text-2xl font-bold mb-4&quot;&amp;gt;활성 유저 목록 ({users.length}명)&amp;lt;/h1&amp;gt;
      
      &amp;lt;ul className=&quot;space-y-2&quot;&amp;gt;
        {users.map((user) =&amp;gt; (
          // 3. 렌더링 된 결과(HTML)만 클라이언트로 전송됩니다.
          &amp;lt;li key={user.id} className=&quot;border p-2 rounded&quot;&amp;gt;
            {user.name} ({user.email})
          &amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useState, useEffect가 아예 없다&lt;/li&gt;
&lt;li&gt;따라서 데이터 로딩 상태 관리도 필요 없다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-2번 Client Component로 데이터 전달&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764079013203&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app/post/[id]/page.tsx
import { db } from '@/lib/db';
import LikeButton from '@/components/LikeButton';

export default async function PostDetail({ params }: { params: { id: string } }) {
  // 1. 서버에서 데이터 로딩
  const post = await db.post.findUnique({ where: { id: params.id } });

  return (
    &amp;lt;article&amp;gt;
      &amp;lt;h1&amp;gt;{post.title}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;{post.content}&amp;lt;/p&amp;gt;
      
      {/* 2. 데이터를 Props로 넘겨줌 (직렬화 가능한 값만!) */}
      {/* 함수(onClick 핸들러 등)는 여기서 만들어서 넘길 수 없음 */}
      &amp;lt;LikeButton initialCount={post.likeCount} postId={post.id} /&amp;gt;
    &amp;lt;/article&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LinkButton은 브라우저로 전송&lt;/li&gt;
&lt;li&gt;로직과 DB 코드는 서버에 남음&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-3번 스트리밍 방식&lt;/p&gt;
&lt;pre id=&quot;code_1764079102055&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// app/dashboard/page.tsx
import { Suspense } from 'react';
import RevenueChart from '@/components/RevenueChart'; // 서버 컴포넌트라고 가정
import UserList from '@/components/UserList';         // 서버 컴포넌트라고 가정

export default function Dashboard() {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;대시보드&amp;lt;/h1&amp;gt;
      
      {/* 1. 사용자 목록은 빠르니까 그냥 기다림 */}
      &amp;lt;div className=&quot;mb-8&quot;&amp;gt;
        &amp;lt;UserList /&amp;gt;
      &amp;lt;/div&amp;gt;

      {/* 2. 매출 차트는 3초 걸림 -&amp;gt; 로딩 걸어두고 페이지 먼저 보여줌 */}
      &amp;lt;section className=&quot;h-64 bg-gray-50&quot;&amp;gt;
        &amp;lt;Suspense fallback={&amp;lt;div className=&quot;text-gray-400&quot;&amp;gt;차트 분석 중... ⏳&amp;lt;/div&amp;gt;}&amp;gt;
          {/* RevenueChart 안에서 async/await로 DB 조회 중 */}
          &amp;lt;RevenueChart /&amp;gt; 
        &amp;lt;/Suspense&amp;gt;
      &amp;lt;/section&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무거운 데이터가 전체 페이지 로딩을 막지 않게 하는 패턴&lt;/li&gt;
&lt;li&gt;RevenueChart가 데이터를 다 가져올 때까지 기다리지 않고, 즉시 fallback을 포함한 HTML을 먼저 내려줌&lt;/li&gt;
&lt;li&gt;그리고 데이터 준비가 완료되면 해당 부분만 갈아끼우는 방식&lt;/li&gt;
&lt;li&gt;따라서 Suspense, fallback도 RSC라고 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발 독후감</category>
      <author>5_hyun</author>
      <guid isPermaLink="true">https://shortcoding.tistory.com/585</guid>
      <comments>https://shortcoding.tistory.com/585#entry585comment</comments>
      <pubDate>Tue, 25 Nov 2025 23:04:43 +0900</pubDate>
    </item>
    <item>
      <title>바이브 코딩을 시작해보다</title>
      <link>https://shortcoding.tistory.com/584</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 AI가 빠르게 발전함에 따라서 바이브 코딩을 해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI를 이용하여 빠르게 코딩을 할 수 있지만, 빠르게 하는 것보단 전체적인 흐름을 이해하면서 바이브 코딩을 하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이제 문법 같은 것들은 AI가 전부 해주기 때문에 아키텍처와 기술을 사용하는 이유와 흐름을 많이 공부해야 한다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 프론트와 백 모두 바이브 코딩을 하면서 전체적인 흐름을 이해하는 개발자가 되고자 이 바이브 코딩을 기획하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 사용할 것은 Github에서 개발한 spec-kit과 클로드에서 만든 클로드 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;s&gt;(클로드 결제까지 해서 입에 풀 칠 하고 살아야 한다.)&lt;/s&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 구현하고자 하는 프로젝트는 TestPanda라는 제목을 가졌고, 선생님과 학생들을 위한 사이트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선생님은 문제를 낼 수 있고, 학생들은 문제를 풀 수 있는 사이트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 프로젝트의 기획은 올해 초쯤에 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 이유로 시작을 여태 미뤘지만 이제는 진짜 시작해야 될 때가 된 것 같아서 해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에는 내가 spec-kit과 클로드를 어떻게 활용할 것이고 설정했는지를 정리하고자 한다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;spec-kit&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/github/spec-kit&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/github/spec-kit&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762865245329&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - github/spec-kit:   Toolkit to help you get started with Spec-Driven Development&quot; data-og-description=&quot;  Toolkit to help you get started with Spec-Driven Development - github/spec-kit&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/github/spec-kit&quot; data-og-url=&quot;https://github.com/github/spec-kit&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fhI3U/hyZNpyU2kY/D5oQzJ3hLmmd4UvcKga5pk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/GVjdc/hyZNis0xlC/IzVearaa1b4bS7cTP8CA81/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/github/spec-kit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/github/spec-kit&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fhI3U/hyZNpyU2kY/D5oQzJ3hLmmd4UvcKga5pk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/GVjdc/hyZNis0xlC/IzVearaa1b4bS7cTP8CA81/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - github/spec-kit:   Toolkit to help you get started with Spec-Driven Development&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  Toolkit to help you get started with Spec-Driven Development - github/spec-kit&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;Speckit&quot; 시스템은 프로젝트의 기획부터 실행까지의 과정을 .speckit 디렉토리 내의 Markdown 파일로 관리&lt;/li&gt;
&lt;li&gt;사실 위 링크 문서를 따라하면 어렵지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;순서&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;헌법 만들기
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;/speckit.constitution&amp;nbsp;Create&amp;nbsp;principles&amp;nbsp;focused&amp;nbsp;on&amp;nbsp;code&amp;nbsp;quality,&amp;nbsp;testing&amp;nbsp;standards,&amp;nbsp;user&amp;nbsp;experience&amp;nbsp;consistency,&amp;nbsp;and&amp;nbsp;performance&amp;nbsp;requirements&lt;/li&gt;
&lt;li&gt;코딩을 하면서 지켜야 할 규칙들을 먼저 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;무엇을 만들지 설명&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1762865480829&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/speckit.specify

# 서비스 설명

- **(Core)** 선생님(생성자)은 문제를 출제하여 '시험'을 만들고, 학생(풀이자)들은 이 '시험'에 응시할 수 있는 온라인 시험 플랫폼.
- **(Teacher)** 선생님은 '그룹' 단위로 학생들을 관리하며, 학생들의 시험 성적을 테이블과 차트로 시각화하여 관리함.
- **(Scope)** 초등학생부터 대학생, 스터디 그룹 등 다양한 사용자가 그룹을 만들어 문제를 내고 풀 수 있음.

# 사용자 역할

- **Teacher (문제 생성자):** 그룹을 생성/관리하고, 시험을 출제하며, 학생 성적을 조회/분석함.
- **Student (문제 풀이자):** 그룹에 가입하고, 시험에 응시하며, 자신의 성적을 확인함.
- **(Policy)** 회원가입 시 'Teacher' 또는 'Student' 역할을 선택해야 함. (역할 변경은 추후 관리자 승인 필요)

# 스토리보드

## 1. 공통 (홈, 인증)

### 홈 화면 (로그아웃 상태)

- 서비스의 핵심 기능을 설명하는 랜딩 페이지.
- &quot;문제 만들러 가기&quot; (Teacher용), &quot;시험 응시하기&quot; (Student용) CTA 버튼.
- 로그인/회원가입 모달 버튼.

### 회원가입 (모달)

- **역할 선택:** 'Teacher' / 'Student' 라디오 버튼.
- **로컬 회원가입:** 이메일, 비밀번호, 이름.
- **SNS 회원가입:** 구글, 네이버. (SNS 로그인 시에도 역할 선택 단계를 거쳐야 함)
- **정책:** 이메일 인증 절차를 거쳐야 회원가입이 완료됨.

### 로그인 (모달)

- 로컬 로그인 (이메일, 비밀번호).
- SNS 로그인 (구글, 네이버).
- 비밀번호 찾기 / 재설정 기능.

---

## 2. Teacher (문제 생성자) 대시보드

### 그룹 관리

- **그룹 생성:** 그룹 이름, 설명, 초대 코드(자동 생성)를 설정하여 그룹 생성.
- **그룹 목록:** 내가 생성했거나 속한 그룹 목록 조회.
- **그룹 상세 (관리 페이지):**
    - 그룹 이름/설명 변경.
    - 그룹 삭제 (그룹 내 모든 데이터 삭제).
    - 초대 코드 재발급 또는 비활성화.
- **그룹 인원 관리:**
    - **초대:** 학생들에게 '초대 코드' 또는 '초대 링크'를 공유.
    - **멤버 목록:** 그룹 멤버 목록 조회 (이름, 이메일, 역할).
    - **권한 변경:** 'Student' 멤버를 'Teacher'(공동 출제자)로 권한 변경 가능.
    - **강제 퇴장:** 그룹에서 멤버를 내보내기.

### 시험지 관리 (내가 낸 문제들)

- **시험지 생성:** '시험지' 단위로 문제를 생성함 (예: 2025년 1학기 중간고사).
- **시험지 설정:**
    - 시험지 이름, 설명.
    - 응시 기간 (시작일시 ~ 종료일시).
    - 응시 제한 시간 (예: 60분).
    - 문제 순서 섞기 (셔플) 여부.
    - 성적 공개 시점 (즉시 공개 / 시험 종료 후 공개).
- **문제 추가/수정:** 생성된 시험지에 아래 '문제 타입'에 맞는 문제들을 추가, 수정, 삭제.

### 성적 및 통계

- **대시보드:** 그룹별/시험지별 평균 점수, 응시율 등 핵심 지표 요약.
- **시험별 성적 테이블:**
    - 특정 시험의 모든 학생 성적을 테이블로 표시 (이름, 점수, 응시일시, 소요 시간).
    - **커스텀 컬럼 추가:** 출결, 수행평가 등 오프라인 점수를 최대 5개까지 추가하여 합산 관리.
    - 테이블 데이터 Sort, Filter링, CSV/Excel로 내보내기.
- **시험별 통계 (시각화):**
    - 점수 분포 (히스토그램), 평균, 중앙값, 최솟값/최댓값, 표준편차, 분산.
- **문제별 통계 (시각화):**
    - 각 문제의 정답률 (%), 가장 많이 선택한 오답 (객관식).
- **학생 개인 통계:**
    - 특정 학생의 누적 성적 그래프, 강/약점 분석 (문제 유형별 정답률).

### 채점 관리 (서술형)

- 학생들이 제출한 '서술형' 답안 중 채점이 필요한 목록을 보여줌 (채점 대기 큐).
- 선생님이 직접 답안을 읽고 점수를 입력 (부분 점수 가능).

---

## 3. Student (문제 풀이자) 대시보드

### 그룹 관리

- **그룹 가입:** 선생님에게 받은 '초대 코드'를 입력하여 그룹에 가입.
- **그룹 목록:** 내가 속한 그룹 목록 조회.
- **그룹 탈퇴:** 가입한 그룹에서 스스로 탈퇴 가능.

### 시험 응시

- **응시 가능 목록:** 내가 속한 그룹에서 현재 응시 가능한 시험지 목록.
- **시험 시작:** 제한 시간 타이머 시작.
- **시험 진행:** 문제 풀이 중 답안 자동 임시 저장.
- **시험 제출:** 최종 답안 제출 및 확인.
- **(Policy)** 시험 중 페이지 이탈 시 경고 팝업.

### 내 성적 관리 (내가 푼 문제들)

- **결과 확인:** 내가 응시한 시험 목록과 점수 확인 (선생님의 공개 설정에 따름).
- **상세 리포트:** 시험별로 내가 맞힌 문제, 틀린 문제, 정답, 내 답안 확인.
- **개인 통계:** 나의 전체 평균, 그룹 내 랭킹(선생님이 공개 시), 문제 유형별 정답률.

# 문제 타입 상세 정의

- **( 공통 )** 모든 문제 타입은 '문제 본문'에 이미지, 동영상(URL), 음성 파일을 첨부할 수 있음.
- **( 공통 )** 문제별 배점(점수)을 개별 설정할 수 있어야 함.

## 1. 객관식

- 선택지는 최소 2개, 최대 10개.
- **복수 정답** 허용 여부 설정 가능.
- (자동 채점)

## 2. O / X

- 'O'와 'X' 선택지 2개를 가진 객관식의 특수 형태로 구현.
- (자동 채점)

## 3. 주관식 (단답형)

- 학생이 짧은 단어 또는 구(Phrase)를 직접 입력.
- **복수 정답** 설정 가능 (예: '컴퓨터', 'computer' 둘 다 정답 처리).
- (자동 채점)

## 4. 주관식 (서술형)

- 학생이 긴 문장이나 문단을 입력.
- **(수동 채점)** 선생님이 직접 채점 큐에서 확인하고 점수 부여.

## 5. 순서 배열형

- 주어진 보기(텍스트 또는 이미지)들을 올바른 순서대로 드래그 앤 드롭.
- 예: 역사적 사건 순서, 실험 순서.
- **부분 점수** 가능 (예: 5개 중 3개 순서가 맞으면 60% 점수).
- (자동 채점)

## 6. 연결하기 / 짝짓기

- 왼쪽과 오른쪽 보기(텍스트 또는 이미지)들을 선으로 연결하거나 드래그 앤 드롭으로 짝지음.
- 예: 단어-뜻, 나라-수도.
- **부분 점수** 가능 (예: 4쌍 중 2쌍 맞으면 50% 점수).
- (자동 채점)

## 7. 빈칸 채우기형

- 문장, 수식, 코드 등의 본문 중간에 여러 개의 빈칸(입력란) 생성.
- 빈칸별로 정답(단답형) 설정.
- **부분 점수** 가능 (빈칸 1개당 배점 할당).
- (자동 채점)

## 8. 표 완성형

- 엑셀과 유사한 표(Table)의 특정 셀들을 빈칸으로 제시.
- 학생이 각 빈칸 셀에 정답(단답형)을 입력.
- **부분 점수** 가능 (셀 1개당 배점 할당).
- (자동 채점)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이렇게 내가 원하는 목표를 설정해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 기술 스택 정하기&lt;/p&gt;
&lt;pre id=&quot;code_1762865610812&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/speckit.plan

# 기술 스택

## Frontend

- **Next.js 16** (App Router, React 19)
- **TypeScript**
- **Tailwind CSS** (shadcn/ui의 필수 구성 요소)
- **shadcn/ui** (UI 컴포넌트 라이브러리)
- **Zustand** (가벼운 글로벌 상태 관리)
- **TanStack Query (React Query)** (서버 상태 및 캐시 관리)
- **React Hook Form** (폼 유효성 검사)
- **MSW (Mock Service Worker)** (선택적 API 모킹)

## Backend

- **Java 17**
- **Spring Boot 3.x**
- **Spring Data JPA** (ORM)
- **MySQL** (인증 서비스용)
- **PostgreSQL** (핵심 서비스용)
- **Spring Security** + **JWT** (인증/인가)
- **Spring Cloud Gateway** (MSA의 API 게이트웨이)
- **RabbitMQ** (MSA 간 비동기 통신)
- **Springdoc OpenAPI** (API 문서 자동화)

## DevOps

- **Docker** / **Docker Compose** (로컬 개발 환경)
- **GitHub Actions** (CI/CD)

# 아키텍처 (Infra)

- Backend만 MSA(Microservice Architecture)를 적용함.
- Frontend는 단일 Next.js 애플리케이션으로 구성.
- Backend MSA는 **서비스별 독립 데이터베이스(Polyglot Persistence)** 패턴을 따름:
    1. **인증 서비스 (Auth Service):** (Spring Boot) + **MySQL**
    2. **핵심 서비스 (Core Service):** (Spring Boot) + **PostgreSQL**
- API 게이트웨이(Spring Cloud Gateway)가 모든 외부 요청의 진입점 역할을 함.

# 개발 순서 (추천)

1. **API 계약 정의:** Springdoc OpenAPI를 사용해 각 마이크로서비스의 API 명세를 먼저 정의합니다.
2. **프론트엔드 (MSW 병행):** 정의된 API 명세를 바탕으로 MSW를 사용하여 프론트엔드 개발을 시작합니다. Next.js, shadcn/ui 컴포넌트를 구현합니다.
3. **백엔드 (MSA 구현):** Spring Cloud Gateway를 설정하고, 각 서비스('인증'은 MySQL, '핵심'은 PostgreSQL)를 독립된 Spring Boot 프로젝트로 구현합니다.
4. **통합 및 배포:** 프론트엔드의 MSW를 실제 백엔드 API 호출로 교체하며 통합 테스트를 진행합니다. Docker Compose로 로컬 환경을 구성하고 GitHub Actions로 CI/CD를 구축합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 프론트엔드에서 FSD 폴더 구조를 사용하게 해달라고 했다.&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;4. 작업 분류&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;/speckit.tasks&lt;/li&gt;
&lt;li&gt;tasks.md 파일에다가 앞으로 할 작업들을 미리 계획해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 자동화 작업&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이거는 spec-kit의 기능은 아니지만 내가 필요로 해서 추가했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;package.json (npm 스크립트):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;tasks.md 파일을 쉽게 조회하고 관리할 수 있는 스크립트를 추가했습니다.&lt;/li&gt;
&lt;li&gt;&quot;task:list&quot;: tasks.md의 모든 태스크 목록을 봅니다.&lt;/li&gt;
&lt;li&gt;&quot;task:pending&quot;: 미완료 태스크 10개를 봅니다.&lt;/li&gt;
&lt;li&gt;&quot;task:done&quot;: commit-task.sh 스크립트를 실행하여 태스크 완료와 커밋을 연동합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;commit-task.sh (쉘 스크립트):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;태스크 완료 시 git add .를 실행하고, Conventional Commit 타입과 태스크 설명을 인자로 받아 자동으로 Git 커밋을 생성하는 스크립트입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Claude&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI(저)가 작업을 수행할 때 따라야 할 규칙은 &quot;.claude/instructions.md&quot; 파일에 명확하게 정의되어 있다.&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;1. 자동 커밋 (Auto-Commit)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;**&quot;작고 자주 커밋하라!&quot;**는 원칙&lt;/li&gt;
&lt;li&gt;&lt;b&gt;즉시 커밋:&lt;/b&gt; 파일 1개 생성, 함수 1개 완성, 설정 1개 수정 등 의미 있는 작업 단위가 완료되면, &lt;b&gt;AI가 스스로 판단하여 즉시 커밋&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;금지 사항:&lt;/b&gt; 여러 파일을 한 번에 커밋하거나, 사용자가 &quot;커밋해줘&quot;라고 요청할 때까지 기다리는 것을 금지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Git 커밋 메시지 규칙&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.speckit/constitution.md와 .claude/instructions.md 양쪽에서 강조된 규칙&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Conventional Commits:&lt;/b&gt; feat:, fix:, docs:, chore: 등 정해진 타입을 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한국어 작성:&lt;/b&gt; 모든 커밋 메시지는 반드시 한국어로 작성하도록 지시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 태스크 관리 및 연동 (Workflow)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;세션 시작 시:&lt;/b&gt; AI는 작업을 시작하거나 세션이 재연결될 때마다 &lt;b&gt;.speckit/tasks.md 파일을 먼저 확인&lt;/b&gt;하여 현재까지 완료된 작업과 다음에 할 작업을 파악&lt;/li&gt;
&lt;li&gt;&lt;b&gt;태스크 완료 시:&lt;/b&gt; AI가 tasks.md의 태스크 하나를 완료하면, 해당 항목의 체크박스를 [ ]에서 [x]로 수정한 뒤, &lt;b&gt;이 변경 사항 자체를 즉시 커밋&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 코딩 스타일 및 아키텍처&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;FSD (Feature-Sliced Design):&lt;/b&gt; 프론트엔드 파일 생성 시 반드시 FSD 아키텍처(app, processes, pages, widgets, features, entities, shared)를 준수해야 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한국어 주석:&lt;/b&gt; 코드 내 모든 주석은 한국어로 작성하도록 constitution.md에 명시&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;깃허브&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/5hyun/test-panda&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/5hyun/test-panda&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762865989072&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - 5hyun/test-panda&quot; data-og-description=&quot;Contribute to 5hyun/test-panda development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/5hyun/test-panda&quot; data-og-url=&quot;https://github.com/5hyun/test-panda&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/4kwyX/hyZNrjbChT/q327cckLTlTZIFy4SQs1ok/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/byS7GN/hyZNIc4zi8/MzTjVSd0xwWNL2XqiCrkK0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/5hyun/test-panda&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/5hyun/test-panda&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/4kwyX/hyZNrjbChT/q327cckLTlTZIFy4SQs1ok/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/byS7GN/hyZNIc4zi8/MzTjVSd0xwWNL2XqiCrkK0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - 5hyun/test-panda&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to 5hyun/test-panda development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TestPanda(바이브 코딩)</category>
      <category>claude code</category>
      <category>speck-kit</category>
      <category>바이브 코딩</category>
      <author>5_hyun</author>
      <guid isPermaLink="true">https://shortcoding.tistory.com/584</guid>
      <comments>https://shortcoding.tistory.com/584#entry584comment</comments>
      <pubDate>Tue, 11 Nov 2025 22:00:37 +0900</pubDate>
    </item>
    <item>
      <title>FE 아키텍처 변경과 이유 with 셸 앱(Sheel App)</title>
      <link>https://shortcoding.tistory.com/583</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;1054&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SmaR7/btsPJt5XWzd/ErTgtYDl7g1MskBLdCQA3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SmaR7/btsPJt5XWzd/ErTgtYDl7g1MskBLdCQA3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SmaR7/btsPJt5XWzd/ErTgtYDl7g1MskBLdCQA3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSmaR7%2FbtsPJt5XWzd%2FErTgtYDl7g1MskBLdCQA3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1918&quot; height=&quot;1054&quot; data-origin-width=&quot;1918&quot; data-origin-height=&quot;1054&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Vue3와 Next를 같이 사용해서 프론트엔드 MSA를 구현하려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 Next로 프로젝트를 하는 것이 주 목표였고, Vue3는 부가적으로 가져가는 것이 목표였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드를 다 만들고 나서 프론트엔드를 생각해 보는데, 문득 어떻게 구현을 해야 할지를 생각해 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vue와 React를 합친 MSA는 &lt;b&gt;셸 앱(Shell App)&lt;/b&gt;으로 구현이 가능하다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Next는 좀 다르다. 그 이유를 아래에서 설명해 보겠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;셸 앱(Shell App)이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셸 앱은 마이크로 프론트엔드 아키텍처의 &lt;b&gt;'뼈대' 또는 '컨테이너'&lt;/b&gt; 역할을 하는 핵심 애플리케이션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 쉬운 비유는 &lt;b&gt;스마트폰의 홈 화면&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;셸 앱 (Shell App) = 스마트폰 홈 화면:&lt;/b&gt; 상단 상태 바, 하단 독(Dock)처럼 항상 고정되어 보이는 공통 UI를 가지고 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마이크로 앱 (Micro App) = 개별 앱 아이콘:&lt;/b&gt; 사용자가 특정 앱 아이콘을 누르면, 홈 화면의 메인 영역에 해당 앱이 실행된다. 다른 앱을 실행하면 이전 앱은 사라지고 그 자리에 새로운 앱이 나타난다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 셸 앱은 전체 애플리케이션의 기본 틀을 잡고, URL이나 사용자 행동에 따라 필요한 마이크로 앱을 &lt;b&gt;'불러와서'&lt;/b&gt; 지정된 공간에 &lt;b&gt;'끼워 넣는'&lt;/b&gt; 역할&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;셸 앱의 핵심 책임&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;공통 레이아웃 렌더링:&lt;/b&gt; 헤더, 푸터, 네비게이션 메뉴 등 공통 영역을 렌더링하고 유지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라우팅 (Routing):&lt;/b&gt; URL을 해석하여 어떤 마이크로 앱을 보여줘야 할지 결정하는 '교통정리' 역할&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인증 및 전역 상태 관리:&lt;/b&gt; 로그인 상태, 인증 토큰 등 모든 마이크로 앱이 공유해야 하는 전역 상태를 중앙에서 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마이크로 앱 로딩 및 생명주기 관리:&lt;/b&gt; 필요한 마이크로 앱을 동적으로 불러오고(Lazy Loading), 화면에 렌더링(Mount) 및 제거(Unmount)하는 전체 생명주기를 책임&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MFE 역할에 Vue/React가 적합하고 Next.js가 부적합한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면, Vue/React는 부품(라이브러리)으로 설계되었고, Next.js는 완성된 자동차(프레임워크)로 설계되었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로 프론트엔드는 &lt;b&gt;여러 개의 '부품'을 하나의 '자동차(셸 앱)'에 조립하는 방식&lt;/b&gt;이므로, Vue/React가 그 역할에 훨씬 적합하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Vue / React (라이브러리)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설계 철학:&lt;/b&gt; Vue와 React의 핵심은 UI를 렌더링하는 '뷰(View) &lt;b&gt;라이브러리&lt;/b&gt;'입니다. 이들은 스스로 완전한 애플리케이션이 되려고 하지 않습니다. 라우팅, 상태 관리 등은 개발자가 다른 라이브러리를 선택하여 조립해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;역할:&lt;/b&gt; 이들의 역할은 명확하다. &quot;데이터를 주면, 화면의 특정 부분을 그려주는 것&quot;입니다. 마치 자동차의 '엔진'처럼, 하나의 기능에 집중한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MFE 적합도:&lt;/b&gt; 이러한 특징 덕분에 Vue/React로 만든 앱은 독립적인 '부품'이 되기 매우 쉽다. 셸 앱이 &quot;이 &amp;lt;div&amp;gt; 안에 너 자신을 그려줘&quot;라고 명령하면, Vue/React 앱은 그저 자신의 역할에 맞게 UI를 렌더링 해주면 그만이다. 셸 앱의 통제에 순응하기 좋은 구조이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Next.js (프레임워크)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설계 철학:&lt;/b&gt; Next.js는 단순한 뷰 라이브러리가 아니라, 자체 서버, 파일 기반 라우팅 시스템, 서버 사이드 렌더링(SSR) 기능까지 모두 내장한 &lt;b&gt;풀스택 프레임워크&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;역할:&lt;/b&gt; Next.js는 스스로가 '완성된 자동차'가 되도록 설계되었다. 자체적인 규칙과 생명주기를 가지고 전체 애플리케이션을 지배하려고 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MFE 적합도:&lt;/b&gt; '완성된 자동차(Next.js)'를 다른 '완성된 자동차(셸 앱)' 안에 넣으려고 하면 충돌이 발생한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;라우터 충돌:&lt;/b&gt; 셸 앱의 라우터와 Next.js의 라우터 중 누가 URL을 통제해야 할까?&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버 충돌:&lt;/b&gt; 셸 앱의 서버와 Next.js의 서버는 어떻게 연동해야 할까? SSR 결과물은 누가 만들어서 합쳐야 할까?&lt;/li&gt;
&lt;li&gt;이처럼 Next.js는 스스로가&lt;b&gt; '주인'이 되려는 성격이 강해서&lt;/b&gt;, 다른 앱의 '부품'으로 들어가기에는 너무 무겁고 복잡하며 많은 문제를 일으킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>MSA 공부</category>
      <category>셸 앱</category>
      <author>5_hyun</author>
      <guid isPermaLink="true">https://shortcoding.tistory.com/583</guid>
      <comments>https://shortcoding.tistory.com/583#entry583comment</comments>
      <pubDate>Wed, 6 Aug 2025 23:34:07 +0900</pubDate>
    </item>
    <item>
      <title>Nest, Yarn Berry + WebStorm에서 라이브러리 Not Found 해결 방법</title>
      <link>https://shortcoding.tistory.com/582</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Nest로 블로그 백엔드를 열심히 만들고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리들이 다 not found가 떠서 굉장히 당황을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 실행을 해보니까 서비스가 잘 돌아갔다. 원인은 바로 yarn berry로 설정했기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Yarn Berry 환경에서 &quot;Not Found&quot; 에러가 떴던 이유는, Yarn Berry가 기존의 node_modules 폴더를 사용하는 방식과 완전히 다르게 작동하기 때문&lt;/li&gt;
&lt;li&gt;웹스톰(IDE)은 node_modules 폴더를 기준으로 라이브러리를 찾도록 설계되었는데, Yarn Berry는 그 폴더를 만들지 않으니 IDE 입장에서는 &quot;라이브러리가 설치되지 않았다&quot;고 착각함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&quot;코드는 잘 돌아가는데, 왜 IDE에서만 오류가 났는가?&quot;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;실행 환경 (Node.js)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;터미널에서 yarn start로 프로젝트를 실행하면, Yarn이 Node.js를 실행시키면서 .pnp.cjs라는 &quot;지도&quot;를 함께 건네줌&lt;/li&gt;
&lt;li&gt;Node.js는 이 지도를 보고 라이브러리들을 아무 문제 없이 찾아냄&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개발 환경 (WebStorm)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹스톰은 이 &quot;지도&quot;의 존재를 모름&lt;/li&gt;
&lt;li&gt;웹스톰은 여전히 옛날 방식대로 node_modules 폴더를 찾아 헤매고 있음&lt;/li&gt;
&lt;li&gt;폴더가 없으니 당연히 라이브러리를 &quot;찾을 수 없다(not found)&quot;고 판단하고 빨간 줄을 그었던 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;STEP 1: &lt;span&gt;Yarn PnP SDK 설치&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752668557782&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;yarn dlx @yarnpkg/sdks vscode&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;이 명령어는 프로젝트 루트에 &lt;/span&gt;&lt;span&gt;.yarn/sdks&lt;/span&gt;&lt;span&gt; 폴더를 생성한다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;이 폴더 안에는 웹스톰이 TypeScript와 ESLint를 올바르게 인식하는 데 필요한 파일들이 들어있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;STEP 2: 웹스톰의 TypeScript 경로 설정&lt;/span&gt;&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;TypeScript 설정 메뉴로 이동&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Languages &amp;amp; Frameworks&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;TypeScript&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;733&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k3Gat/btsPmVHBpW5/M8gCmZmZkcuylndkpNE2Z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k3Gat/btsPmVHBpW5/M8gCmZmZkcuylndkpNE2Z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k3Gat/btsPmVHBpW5/M8gCmZmZkcuylndkpNE2Z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk3Gat%2FbtsPmVHBpW5%2FM8gCmZmZkcuylndkpNE2Z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;994&quot; height=&quot;733&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;733&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;~/Desktop/BlogServer-Nest/.yarn/sdks/typescript&quot;&amp;nbsp;&lt;/b&gt;이미지에는 이렇게 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 나는 처음에 이렇게 넣었을 때, 잘못된 경로라고 떠서 매우 당황스러웠다.&lt;/p&gt;
&lt;pre id=&quot;code_1752668733267&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/Users/{USER}/Desktop/BlogServer-Nest/.yarn/sdks/typescript/lib&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 직접 파일 경로를 찾아가서 이거를 저 경로 부분에 넣었더니 설정이 제대로 되었다.&lt;/p&gt;</description>
      <category>MSA 공부</category>
      <category>NEST</category>
      <category>웹스톰</category>
      <author>5_hyun</author>
      <guid isPermaLink="true">https://shortcoding.tistory.com/582</guid>
      <comments>https://shortcoding.tistory.com/582#entry582comment</comments>
      <pubDate>Wed, 16 Jul 2025 21:27:08 +0900</pubDate>
    </item>
    <item>
      <title>MSA에서 백엔드는 어떻게 인증/인가를 할까</title>
      <link>https://shortcoding.tistory.com/581</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot로 회원가입/로그인을 만들고, Nest로 블로그 서비스 CRUD를 만들다 보니까 들은 생각이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA는 서로 인증/인가를 어떻게 할까? 처음에는 Spring Boot로 만든 인증/인가 서비스에서 모든 처리를 하면 되겠다고 생각을 했다. 하지만 이 방식은 뭔가 허술하다는 것을 느꼇고, MSA에서 인증/인가를 구현하는 방식을 찾아보고 그 중에서 제일 괜찮은 방식을 적용해야 겠다고 생각했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. API 게이트웨이&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 게이트웨이는 마이크로서비스 아키텍처에서 클라이언트와 백엔드 서비스 사이의 프록시(Proxy) 서버 역할을 하는 핵심 컴포넌트&lt;/li&gt;
&lt;li&gt;인증/인가 관점에서 게이트웨이는 모든 외부 요청에 대한 단일 진입점(Single Point of Entry)으로 동작하며, 보안 관련 로직을 중앙에서 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;과정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Step 1: 게이트웨이 필터/미들웨어에서의 인증 처리&lt;/span&gt;&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;span&gt;API 게이트웨이는 라우팅 로직을 실행하기 전에, 사전에 정의된 &lt;/span&gt;&lt;b&gt;&lt;span&gt;필터 체인(Filter Chain)&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 또는 &lt;b&gt;미들웨어(Middleware)&lt;/b&gt;를 통해 요청을 처리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;이 필터 중 하나가 &lt;b&gt;인증 필터(Authentication Filter)&lt;/b&gt;의 역할을 수행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;인증 필터의 핵심 로직:&lt;/span&gt;&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;Authorization&lt;/span&gt;&lt;span&gt; 헤더에서 &lt;b&gt;JWT(JSON Web Token)&lt;/b&gt;를 추출&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;미리 설정된 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Secret Key&lt;/span&gt;&lt;/b&gt;&lt;span&gt;나 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Public Key&lt;/span&gt;&lt;/b&gt;&lt;span&gt;를 사용하여 토큰의 서명(Signature)을 검증합니다. 서명이 유효하지 않으면, 이 요청은 위조된 것으로 간주&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;토큰의 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Payload&lt;/span&gt;&lt;/b&gt;&lt;span&gt;에 포함된 만료 시간(&lt;/span&gt;&lt;span&gt;exp&lt;/span&gt;&lt;span&gt; claim)을 현재 시간과 비교하여 토큰이 만료되지 않았는지 확인&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;발급자(&lt;/span&gt;&lt;span&gt;iss&lt;/span&gt;&lt;span&gt; claim) 등 기타 클레임들이 유효한지 검증&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;만약 위 과정 중 하나라도 실패하면, 게이트웨이는 즉시 &lt;/span&gt;&lt;b&gt;&lt;span&gt;401 Unauthorized&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 또는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;403 Forbidden&lt;/span&gt;&lt;/b&gt;&lt;span&gt; HTTP 상태 코드로 응답하고 요청 처리를 중단&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;이로써 유효하지 않은 트래픽이 내부망으로 유입되는 것을 원천 차단&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Step 2: 요청 재구성 및 다운스트림 서비스로 전파&lt;/span&gt;&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;span&gt;인증에 성공하면, 게이트웨이는 다운스트림(내부) 서비스가 신뢰하고 사용할 수 있도록 요청을 재구성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;헤더 변조/추가:&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;보안을 위해 외부에서 사용된 &lt;/span&gt;&lt;span&gt;Authorization&lt;/span&gt;&lt;span&gt; 헤더를 제거&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;검증된 JWT의 Payload에서 사용자 식별자(&lt;/span&gt;&lt;span&gt;sub&lt;/span&gt;&lt;span&gt; claim, 보통 User ID)나 역할(Role) 정보를 추출하여, 신뢰할 수 있는 새로운 내부용 헤더에 담아줌 (예: &lt;/span&gt;&lt;span&gt;X-User-ID: 12345&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;X-User-Roles: ADMIN,USER&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;이러한 헤더 추가는 다운스트림 서비스가 사용자를 신뢰하고 인가 처리를 쉽게 할 수 있도록 함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Step 3: 라우팅 (Routing)&lt;/span&gt;&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;span&gt;게이트웨이는 요청 경로(예: &lt;/span&gt;&lt;span&gt;/orders/**&lt;/span&gt;&lt;span&gt;)를 기반으로 사전에 정의된 라우팅 테이블을 참조하여, 이 요청을 처리할 적절한 내부 마이크로서비스(예: &lt;/span&gt;&lt;span&gt;order-service&lt;/span&gt;&lt;span&gt;)로 프록시(Proxy)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;기술적 장점&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;관심사의 분리 (Separation of Concerns)&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;비즈니스 로직을 처리하는 마이크로서비스 코드에서 인증/보안 관련 코드가 완전히 분리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;서비스 개발자는 보안 라이브러리나 Secret Key 관리에 신경 쓸 필요 없이 비즈니스 기능 개발에 집중&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;중앙 집중화된 보안 관리&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;보안 정책 변경(예: 토큰 만료 시간 변경, 암호화 알고리즘 교체)이 필요할 때, 여러 서비스를 수정할 필요 없이 API 게이트웨이 한 곳만 수정하면 됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;보안 경계 형성 (Security Perimeter)&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;외부와 내부 네트워크 사이에 명확한 보안 경계를 만들어, 인증되지 않은 어떤 요청도 내부 시스템에 도달하지 못하게 막는 방화벽 역할을 수행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;성능 향상&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;다운스트림 서비스들이 매 요청마다 수행해야 할 암호화 검증 작업을 게이트웨이가 대신 처리해주므로, 전체 시스템의 부하를 줄임&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;2. 내부 JWT(&lt;/span&gt;서비스 간(Inter-Service) JWT 인증)&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 방식은 외부 사용자를 위한 인증과 별개로, 내부 마이크로서비스끼리 통신할 때 서로의 신원을 확인하기 위해 별도의 JWT를 사용하는 방법&lt;/li&gt;
&lt;li&gt;결국 클라이언트와 인증/인가 서비스는 또 따로 있어야 함&lt;/li&gt;
&lt;li&gt;중앙 인증 서비스(Auth Service)가 각 서비스에게 고유한 JWT를 발급하고, 서비스들은 다른 서비스를 호출할 때 이 JWT를 사용해 자신의 신원을 증명&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;과정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 1: 서비스 고유의 JWT 발급 요청 및 수신&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;블로그 서비스(Nest)가 시작될 때, 또는 주기적으로 중앙 인증 서비스(Spring Boot)에 자신의 서비스 이름(예: blog-service)과 같은 식별 정보를 보내 내부 통신용 JWT를 요청&lt;/li&gt;
&lt;li&gt;인증 서비스는 요청한 서비스의 신원을 확인한 후, 해당 서비스만을 위한 &lt;b&gt;내부용 JWT(Internal JWT)&lt;/b&gt;를 발급해 응답
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 JWT에는 sub: 'blog-service' 와 같이 &lt;b&gt;서비스의 이름&lt;/b&gt;이 포함&lt;/li&gt;
&lt;li&gt;사용자용 JWT보다 &lt;b&gt;훨씬 짧은 만료 시간&lt;/b&gt;(예: 5분, 10분)을 가짐&lt;/li&gt;
&lt;li&gt;호출 가능한 서비스 목록(aud: ['user-service', 'payment-service']) 등 더 세분화된 권한 정보를 포함할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 2: 내부 JWT를 이용한 서비스 간 호출&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;이제 블로그 서비스가 특정 유저 정보를 얻기 위해 회원 서비스(Spring Boot)를 호출해야 하는 상황을 가정&lt;/li&gt;
&lt;li&gt;블로그 서비스는 회원 서비스를 호출할 때, Authorization: Bearer &amp;lt;내부용 JWT&amp;gt; 헤더에 Step 1에서 발급받은 &lt;b&gt;내부용 JWT&lt;/b&gt;를 담아 보냄
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때 API에서 받은 &lt;b&gt;사용자 정보 헤더&lt;/b&gt;(예: X-User-ID: 123)도 함께 전달하여, &quot;내가 블로그 서비스인데, 123번 사용자의 정보를 요청한다&quot;는 의미를 명확히 밝힘&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 3: 호출받은 서비스에서의 내부 JWT 검증&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;요청을 받은 회원 서비스는 Authorization 헤더의 내부용 JWT를 검증&lt;/li&gt;
&lt;li&gt;이때 검증은 인증 서비스와 공유된 &lt;b&gt;Secret Key&lt;/b&gt; 또는 &lt;b&gt;Public Key&lt;/b&gt;를 사용&lt;/li&gt;
&lt;li&gt;JWT가 유효하고, 토큰에 명시된 sub 클레임이 신뢰할 수 있는 서비스(예: blog-service)임이 확인되면 요청을 처리&lt;/li&gt;
&lt;li&gt;유효하지 않으면 403 Forbidden으로 응답하여 비인가 서비스의 접근을 차단&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기술적 장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Zero-Trust 네트워크 구현&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;내부망은 안전하다&quot;는 가정을 버리고, 모든 서비스 간 통신에 대해 인증을 강제하여 내부의 잠재적 위협으로부터 시스템을 보호&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세분화된 접근 제어&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스별로 호출할 수 있는 다른 서비스의 목록을 JWT에 명시하여, A 서비스는 B 서비스만 호출할 수 있고 C 서비스는 호출할 수 없도록 정교한 제어가 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 신원 명확화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 서비스가 요청했는지 명확하게 식별하고 로그로 남길 수 있어, 문제 발생 시 추적 및 디버깅이 용이&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;게이트웨이 의존성 감소&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 게이트웨이가 모든 것을 처리하는 대신, 각 서비스가 자신의 보안을 일부 책임지는 구조로 분산되어 게이트웨이의 부하를 줄일 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>MSA 공부</category>
      <category>MSA</category>
      <category>인증/인가</category>
      <author>5_hyun</author>
      <guid isPermaLink="true">https://shortcoding.tistory.com/581</guid>
      <comments>https://shortcoding.tistory.com/581#entry581comment</comments>
      <pubDate>Tue, 15 Jul 2025 23:42:43 +0900</pubDate>
    </item>
    <item>
      <title>인증/인가, Spring Boot 서버 구축</title>
      <link>https://shortcoding.tistory.com/580</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 첫 번째 프로젝트인 Spring Boot로 구현된 JWT 기반 인증 서버 프로젝트를 정리해 보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[프로젝트 GitHub 링크]&lt;/b&gt; &lt;a href=&quot;https://github.com/5hyun/AuthServer-SpringBoot&quot;&gt;https://github.com/5hyun/AuthServer-SpringBoot&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751712336611&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - 5hyun/AuthServer-SpringBoot&quot; data-og-description=&quot;Contribute to 5hyun/AuthServer-SpringBoot development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/5hyun/AuthServer-SpringBoot&quot; data-og-url=&quot;https://github.com/5hyun/AuthServer-SpringBoot&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bGMCGG/hyZgbOZttC/ca9Jkba1i4kuNGELblRH50/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/m6zNj/hyZf8Sf1Dp/jKOKYGnkC2VKyVjNDaFzW0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/5hyun/AuthServer-SpringBoot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/5hyun/AuthServer-SpringBoot&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bGMCGG/hyZgbOZttC/ca9Jkba1i4kuNGELblRH50/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/m6zNj/hyZf8Sf1Dp/jKOKYGnkC2VKyVjNDaFzW0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - 5hyun/AuthServer-SpringBoot&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to 5hyun/AuthServer-SpringBoot development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  프로젝트 개요&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주요 기능&lt;/b&gt;: 사용자 회원가입 및 로그인, JWT(JSON Web Token)를 사용한 인증, Access/Refresh Token을 이용한 토큰 갱신, Redis를 활용한 로그아웃 및 토큰 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 기술&lt;/b&gt;: Spring Security를 통해 보안을 강화하고, JPA로 데이터를 관리하며, Redis로 서버의 상태를 효율적으로 다루는 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  폴더 및 파일별 상세 역할&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  src/main/java/com/example/AuthServer&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 소스 코드가 위치하는 메인 디렉토리로, 기능별로 패키지가 잘 분리되어 있다.&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;  AuthServerApplication.java&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: Spring Boot 애플리케이션의 &lt;b&gt;시작점&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상세 설명&lt;/b&gt;: @SpringBootApplication 어노테이션이 핵심. main 메소드에서 SpringApplication.run()을 호출하여 내장 웹 서버(Tomcat)를 실행하고 애플리케이션을 구동한다. @OpenAPIDefinition은 Swagger API 문서의 기본 정보를 설정하는 역할을 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;⚙️ config 패키지&lt;/b&gt; 애플리케이션의 주요 설정을 담당하는 클래스들이 모여있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SecurityConfig.java&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: Spring Security 관련 설정을 총괄&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상세 설명&lt;/b&gt;: @EnableWebSecurity로 Spring Security를 활성화하고, passwordEncoder()로 비밀번호를 암호화한다. securityFilterChain()에서는 HTTP 요청에 대한 보안 규칙(CSRF 비활성화, Stateless 세션 관리, URL별 접근 권한 설정)과 JwtAuthenticationFilter를 등록하여 JWT 토큰을 먼저 검사하도록 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RedisConfig.java&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: Redis 데이터베이스 연결 및 사용 설정을 담당&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상세 설명&lt;/b&gt;: application.yml의 정보를 바탕으로 Redis 연결을 설정하고, RedisTemplate을 Bean으로 등록하여 서비스 로직에서 쉽게 Redis를 사용하도록 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;jwt 하위 패키지&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;JwtTokenProvider.java&lt;/b&gt;: JWT 생성, 검증, 정보 추출 등 JWT 관련 모든 핵심 로직을 담당하는 유틸리티 클래스이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JwtAuthenticationFilter.java&lt;/b&gt;: 클라이언트의 모든 요청에 대해 JWT 토큰을 검사하는 커스텀 필터. 요청 헤더에서 토큰을 추출해 유효성을 검증하고, 유효한 경우 SecurityContextHolder에 인증 정보를 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;  controller 패키지&lt;/b&gt; 애플리케이션의 API 엔드포인트를 정의하는 컨트롤러가 위치한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;UserController.java&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 사용자 관련 HTTP 요청을 받아 처리하는 API 엔드포인트를 정의한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상세 설명&lt;/b&gt;: @RestController를 통해 모든 메소드가 JSON 데이터를 반환한다. 회원가입, 로그인, 로그아웃, 토큰 갱신 등의 API가 정의되어 있으며, 클라이언트의 요청을 받아 UserService로 비즈니스 로직 처리를 위임한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;  domain 패키지&lt;/b&gt; 데이터베이스 테이블과 매핑되는 JPA 엔티티(Entity) 클래스가 위치한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;User.java&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 데이터베이스의 users 테이블과 매핑되는 JPA 엔티티 클래스.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상세 설명&lt;/b&gt;: @Entity 어노테이션으로 JPA 엔티티임을 나타낸다. 사용자의 정보를 필드로 가지며, Spring Security의 UserDetails 인터페이스를 구현하여 User 객체 자체를 Spring Security에서 인증 주체로 사용할 수 있게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;  dto (Data Transfer Object) 패키지&lt;/b&gt; 계층 간 데이터 전송을 위해 사용되는 객체들을 정의한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SignUpRequest.java: 회원가입 요청 데이터를 담으며 유효성 검사 어노테이션이 포함되어 있다.&lt;/li&gt;
&lt;li&gt;LoginRequest.java: 로그인 요청 데이터를 담는다.&lt;/li&gt;
&lt;li&gt;TokenInfo.java: 로그인 성공 시 발급되는 Access/Refresh Token 정보를 담아 클라이언트에 전달한다.&lt;/li&gt;
&lt;li&gt;ErrorResponse.java: 예외 발생 시 일관된 형식의 에러 메시지를 전달하기 위해 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;  exception 패키지&lt;/b&gt; 애플리케이션 전역의 예외를 처리하는 클래스가 위치한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GlobalExceptionHandler.java&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 애플리케이션 전역에서 발생하는 예외를 중앙에서 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상세 설명&lt;/b&gt;: @RestControllerAdvice를 통해 모든 컨트롤러의 예외를 감지하고, @ExceptionHandler를 사용하여 특정 예외 타입별로 적절한 HTTP 상태 코드와 에러 메시지를 생성하여 응답한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt; ️ repository 패키지&lt;/b&gt; 데이터베이스 작업을 처리하는 JPA 리포지터리 인터페이스가 위치한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;UserRepository.java&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: User 엔티티에 대한 데이터베이스 작업을 처리하는 인터페이스.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상세 설명&lt;/b&gt;: JpaRepository&amp;lt;User, Long&amp;gt;를 상속받아 기본적인 CRUD 메소드가 자동으로 생성된다. findByEmail(String email)처럼 정해진 규칙에 따라 메소드를 선언하면 Spring Data JPA가 쿼리를 자동으로 생성해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt; &amp;zwj;  service 패키지&lt;/b&gt; 애플리케이션의 핵심 비즈니스 로직을 구현한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;UserService.java&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 사용자 관련 핵심 비즈니스 로직을 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상세 설명&lt;/b&gt;: 회원가입(비밀번호 암호화, 중복 체크), 로그인(인증 및 토큰 발급), 로그아웃(토큰 블랙리스트 처리), 토큰 갱신 로직이 구현되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CustomUserDetailsService.java&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: Spring Security의 UserDetailsService를 구현하여, 사용자 인증 시 필요한 정보를 DB에서 조회하는 역할을 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  src/main/resources&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;application.yml&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: 애플리케이션의 주요 외부 설정을 담당한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상세 설명&lt;/b&gt;: Redis 서버 주소, 로깅 레벨 등 애플리케이션 동작에 필요한 다양한 설정값들을 정의한다. (DB 정보, JWT secret key 등은 보통 별도 파일로 분리하여 관리한다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  기타 최상위 파일&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;docker-compose.yml&lt;/b&gt;: Docker를 사용하여 MySQL과 Redis를 컨테이너 환경으로 손쉽게 구성하고 실행하기 위한 설정 파일.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;README.md&lt;/b&gt;: 프로젝트에 대한 전반적인 소개, 기술 스택 등을 담고 있는 프로젝트 설명서.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;.gitignore&lt;/b&gt;: Git 버전 관리에서 추적하지 않을 파일 및 폴더 목록을 정의. 빌드 결과물이나 민감 정보가 원격 저장소에 올라가지 않도록 방지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  AuthServer-SpringBoot 프로젝트 흐름도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름도는 사용자가 &lt;b&gt;로그인&lt;/b&gt;을 요청했을 때, 서버 내부에서 어떤 일들이 순서대로 일어나는지 보여줍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  사용자 로그인 (POST /api/users/login) 요청 흐름&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 클라이언트 (Client)&lt;/b&gt; 사용자가 아이디(이메일)와 비밀번호를 입력하고 '로그인' 버튼을 클릭합니다. 클라이언트는 서버의 /api/users/login 엔드포인트로 POST 요청을 보냅니다. (Request Body에 LoginRequest DTO 형식으로 이메일, 비밀번호 포함)&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;&lt;b&gt;2. DispatcherServlet &amp;amp; 필터(Filter) 체인&lt;/b&gt; Spring의 DispatcherServlet이 가장 먼저 요청을 받습니다. SecurityConfig에 등록된 필터 체인을 통과합니다.&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;JwtAuthenticationFilter (config/jwt 패키지)&lt;/b&gt;: /api/users/login 경로는 인증 없이 접근 가능(permitAll())하도록 SecurityConfig에 설정되어 있으므로, 이 단계에서는 특별한 토큰 검증 없이 다음 단계로 요청을 전달합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 컨트롤러 (Controller)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;UserController (controller 패키지)&lt;/b&gt; @PostMapping(&quot;/login&quot;) 어노테이션이 붙은 login() 메소드가 요청을 처리합니다. @RequestBody를 통해 받은 JSON 데이터를 LoginRequest DTO 객체로 변환합니다. 이 DTO 객체를 UserService의 login() 메소드로 전달하여 실제 비즈니스 로직 처리를 위임합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 서비스 (Service)&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;UserService (service 패키지)&lt;/b&gt; login(email, password) 메소드가 호출됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1단계: 인증용 객체 생성&lt;/b&gt; 전달받은 이메일과 비밀번호로 UsernamePasswordAuthenticationToken을 생성합니다. (아직 인증되지 않은 상태의 토큰)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2단계: 실제 인증 위임&lt;/b&gt; AuthenticationManager의 authenticate() 메소드에 위에서 생성한 토큰을 넘겨 인증을 시도합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. Spring Security 인증 과정&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;AuthenticationManager (SecurityConfig에서 Bean으로 등록)&lt;/b&gt; 인증 처리를 위해 등록된 AuthenticationProvider를 호출합니다. 여기서는 CustomUserDetailsService를 사용하도록 설정되어 있습니다. AuthenticationManager는 CustomUserDetailsService에게 사용자 정보를 찾아오라고 요청합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CustomUserDetailsService (service 패키지)&lt;/b&gt; loadUserByUsername(email) 메소드가 실행됩니다. UserRepository를 사용하여 데이터베이스에서 해당 이메일을 가진 사용자를 조회합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UserRepository (repository 패키지)&lt;/b&gt; findByEmail(email) 메소드를 실행하여 DB에서 User 정보를 조회합니다. 조회된 User 엔티티 객체를 CustomUserDetailsService에게 반환합니다. (사용자가 없으면 UsernameNotFoundException 발생)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CustomUserDetailsService &amp;rarr; AuthenticationManager&lt;/b&gt; DB에서 조회한 User 객체(UserDetails 타입)를 AuthenticationManager에게 반환합니다. AuthenticationManager는 UserService로부터 받은 요청 비밀번호와 DB에서 조회한 User의 암호화된 비밀번호를 PasswordEncoder를 통해 비교하여 인증을 최종 완료합니다. (비밀번호가 틀리면 AuthenticationException 발생) 인증이 성공하면, 사용자 정보와 권한이 담긴 &lt;b&gt;인증된 Authentication 객체&lt;/b&gt;를 생성하여 UserService에게 돌려줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. 서비스 (Service) - 후처리&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;UserService (service 패키지)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;3단계: JWT 토큰 생성&lt;/b&gt; AuthenticationManager로부터 성공적으로 인증된 Authentication 객체를 받습니다. 이 객체를 **JwtTokenProvider**에게 전달하여 &lt;b&gt;Access Token&lt;/b&gt;과 &lt;b&gt;Refresh Token&lt;/b&gt;을 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;4단계: Refresh Token 저장&lt;/b&gt; 생성된 Refresh Token을 Redis에 저장합니다. (Key: RT:user@email.com, Value: refreshToken) RedisTemplate을 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;5단계: 최종 결과 반환&lt;/b&gt; TokenInfo DTO에 두 토큰 정보를 담아 UserController에게 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7. 컨트롤러 (Controller) &amp;rarr; 클라이언트 (Client)&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;UserController (controller 패키지)&lt;/b&gt; UserService로부터 받은 TokenInfo 객체를 ResponseEntity에 담아 JSON 형태로 클라이언트에게 응답합니다. (HTTP Status 200 OK)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;8. 클라이언트 (Client)&lt;/b&gt; 서버로부터 받은 Access Token과 Refresh Token을 저장하고, 이후 API 요청 시 Authorization 헤더에 Access Token을 담아 보냅니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[개발 용어] 헷갈리는 API, 엔드포인트, URL, URI 완벽 정리  ️&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. API (Application Programming Interface) &amp;rarr;   레스토랑의 '메뉴판'과 '주문 시스템 규칙'&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;의미&lt;/b&gt;: 프로그램(손님)이 다른 프로그램(레스토랑)의 기능과 데이터를 사용하기 위한 &lt;b&gt;모든 규칙과 약속의 총집합&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비유&lt;/b&gt;: 레스토랑의 &lt;b&gt;메뉴판&lt;/b&gt;과 같습니다. 메뉴판에는 주문 가능한 음식(기능) 목록, 음식 설명, 가격(필요한 데이터), 그리고 &quot;주문은 키오스크에서 해주세요&quot;와 같은 주문 규칙까지 모두 명시되어 있죠. 손님(클라이언트)은 이 **정해진 메뉴와 규칙(API)**에 따라서만 음식을 주문(요청)할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 엔드포인트 (Endpoint) &amp;rarr;   메뉴판의 '김치찌개' 항목과 '주문 창구'&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;의미&lt;/b&gt;: API라는 메뉴판에 명시된 특정 기능을 실행할 수 있는 &lt;b&gt;구체적인 접점 또는 창구&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비유&lt;/b&gt;: 메뉴판에서 &lt;b&gt;'김치찌개(8,000원)' 항목을 손가락으로 가리키는 행위&lt;/b&gt;가 바로 엔드포인트와 연결되는 과정입니다. 이 메뉴 항목을 통해 우리는 '김치찌개'라는 특정 기능을 요청하게 되고, 이 요청은 주방의 &lt;b&gt;'주문 접수 창구'&lt;/b&gt; 로 전달됩니다. 이 창구가 바로 기능을 실행하는 최종 지점이죠.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 예시&lt;/b&gt;: POST /api/users/login 은 '로그인'이라는 특정 메뉴를 주문하는 명확한 창구(엔드포인트)입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. URL (Uniform Resource Locator) &amp;rarr;   레스토랑까지 가는 '상세 주소'&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;의미&lt;/b&gt;: 엔드포인트(주문 창구)가 있는 레스토랑까지 찾아갈 수 있는, &lt;b&gt;'위치'가 명시된 완전한 경로&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비유&lt;/b&gt;: &quot;배달의민족&quot;으로 음식을 주문한다고 생각해 보세요. &lt;b&gt;&quot;서울특별시 강남구 테헤란로 123, A 타워 1층 '개발자 맛집'&quot;&lt;/b&gt; 이라는 정확한 주소가 있어야 배달 기사님이 음식을 픽업하러 갈 수 있습니다. 이처럼 프로토콜(https://), 도메인(A 타워), 상세 경로(1층...)가 모두 포함된 이 '상세 주소'가 바로 URL입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. URI (Uniform Resource Identifier) &amp;rarr;   레스토랑의 '사업자등록번호'&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;의미&lt;/b&gt;: 인터넷의 모든 자원을 &lt;b&gt;고유하게 식별하는 모든 종류의 정보&lt;/b&gt;. 가장 포괄적인 개념입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비유&lt;/b&gt;: 레스토랑의 &lt;b&gt;'사업자등록번호'&lt;/b&gt; 와 같습니다. 이 번호는 전국의 수많은 식당 중에서 이 식당 하나를 유일하게 &lt;b&gt;식별&lt;/b&gt;해 주지만, 이 번호만 보고 식당 위치를 찾아갈 수는 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;관계&lt;/b&gt;: 레스토랑의 '상세 주소(URL)'는 당연히 레스토랑을 식별하는 정보이므로 '사업자등록번호(URI)'의 역할을 겸할 수 있습니다. 따라서, &lt;b&gt;모든 URL은 URI에 포함&lt;/b&gt;됩니다. 하지만 위치 정보가 없는 순수한 식별 정보(예: urn:isbn:1234 같은 책 고유번호)는 URL이 될 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>MSA 공부</category>
      <category>스프링 부트</category>
      <category>인증/인가</category>
      <author>5_hyun</author>
      <guid isPermaLink="true">https://shortcoding.tistory.com/580</guid>
      <comments>https://shortcoding.tistory.com/580#entry580comment</comments>
      <pubDate>Sat, 5 Jul 2025 19:58:07 +0900</pubDate>
    </item>
    <item>
      <title>MSA 초기 아키텍처</title>
      <link>https://shortcoding.tistory.com/579</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에 대해서 알게 된 것은 사실 오래되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 MSA를 적용해 본 적은 없다. MSA는 주로 대규모 프로젝트에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 접할 기회가 별로 없었다. 그렇지만 프로젝트에 적합하지 않다고 해서 공부를 아예 하지 않는 것은 좋지 않다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA를 간단하게 적용해 보고 왜 규모가 큰 프로젝트에 MSA가 도입되었는지, 왜 작은 프로젝트엔 적용하는 것이 안 좋은 것인지 직접 공부하면서 깨우쳐 보려고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아키텍처&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;1026&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNFeWx/btsOMV9pwix/C28NptSkkbU8K69VdSkj5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNFeWx/btsOMV9pwix/C28NptSkkbU8K69VdSkj5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNFeWx/btsOMV9pwix/C28NptSkkbU8K69VdSkj5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNFeWx%2FbtsOMV9pwix%2FC28NptSkkbU8K69VdSkj5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1774&quot; height=&quot;1026&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;1026&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 설계한 아키텍처는 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 다양한 기술 스택을 도입한 이유는 간단하게라도 모든 기술을 적용해 보면서 익히고 싶기 때문에 적용한 것도 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Client
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인/회원가입, 마이페이지
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기술 스택: Vue3&lt;/li&gt;
&lt;li&gt;이유: Vue3를 아직 다뤄 본 적이 없기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;블로그 글 쓰기/보기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기술 스택: Next15&lt;/li&gt;
&lt;li&gt;이유: Next로 아직 프로젝트를 해 본 적이 없기 때문, 블로그 같은 프로젝트는 SSR이 적합하기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BFF
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기술 스택: Nest&lt;/li&gt;
&lt;li&gt;이유: 2개의 서버를 합쳐서 전달해야 하기 때문에 익숙한 JS를 사용하는 것이 적합하기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Server
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증/인가
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기술 스택: Spring Boot + Spring Security + Redis&lt;/li&gt;
&lt;li&gt;이유: Spring Boot를 아직 다뤄 본 적이 없고, Spring Security와 Redis는 검증된 기술이기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;블로그 글 쓰기/보기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기술 스택: Nest&lt;/li&gt;
&lt;li&gt;이유: Nest를 아직 다뤄 본 적이 없기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DataBase
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증/인가
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기술 스택: MySQL&lt;/li&gt;
&lt;li&gt;이유
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 정보(ID, 암호화된 비밀번호, 역할 등)는 관계가 명확하고 정형화된 데이터이고,&lt;/li&gt;
&lt;li&gt;복잡한 비정형 데이터나 고급 기능보다는, 빠르고 일관된 트랜잭션 처리가 더 중요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;블로그 글 쓰기/보기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기술 스택: PostgreSQL&lt;/li&gt;
&lt;li&gt;이유
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게시글에는 제목이나 본문 외에도 '태그', '카테고리', '임시저장 데이터', '관련글 정보' 등 다양한 형태의 메타데이터가 붙을 수 있음&lt;/li&gt;
&lt;li&gt;PostgreSQL의 JSONB 타입을 사용하면, 이러한 비정형 데이터를 스키마 변경 없이 유연하게 저장하고, 내부 필드를 인덱싱하여 빠르게 조회 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&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;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트를 하는 목적은 일단 각 기능들을 최소한의 기능한 만족시키면서 다 완성을 시키는 것이 목표이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 개발을 다 마치고 난다면 그 이후에 부족한 부분을 고치고, 기능을 추가하고, 테스트 코드를 작성하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-순서&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인증/인가, Spring Boot&lt;/li&gt;
&lt;li&gt;블로그 글 쓰기/보기, Nest&lt;/li&gt;
&lt;li&gt;BFF, Nest&lt;/li&gt;
&lt;li&gt;로그인/회원가입, Vue3&lt;/li&gt;
&lt;li&gt;블로그 글 쓰기/보기 + 마이 페이지, Nest + Vue3&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 좋은 방법은 의존성이 없는 가장 깊은 곳(백엔드)부터 개발을 시작하여, 점차 바깥쪽(프론트엔드)으로 나아가는 것이 목표&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계가 끝날 때마다 확실한 성과를 눈으로 확인하며 진행하는 것이 중요&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아키텍처 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025.08.06 FE 아키텍처 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://shortcoding.tistory.com/583&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://shortcoding.tistory.com/583&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1754490897001&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;FE 아키텍처 변경과 이유 with 셸 앱(Sheel App)&quot; data-og-description=&quot;기존에는 Vue3와 Next를 같이 사용해서 프론트엔드 MSA를 구현하려고 했다.사실 Next로 프로젝트를 하는 것이 주 목표였고, Vue3는 부가적으로 가져가는 것이 목표였다.백엔드를 다 만들고 나서 프론&quot; data-og-host=&quot;shortcoding.tistory.com&quot; data-og-source-url=&quot;https://shortcoding.tistory.com/583&quot; data-og-url=&quot;https://shortcoding.tistory.com/583&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ZCjGw/hyZrlYMf6F/GKtkUrLt6G4xXM1j48eGGK/img.png?width=800&amp;amp;height=439&amp;amp;face=0_0_800_439,https://scrap.kakaocdn.net/dn/MXhU9/hyZuGmVCTP/hZBrrkkU6eKkKEn12qCJhk/img.png?width=800&amp;amp;height=439&amp;amp;face=0_0_800_439,https://scrap.kakaocdn.net/dn/bKhzGw/hyZuxXQeOQ/Myzffk6msQZSgEwIJpRY5K/img.png?width=1918&amp;amp;height=1054&amp;amp;face=0_0_1918_1054&quot;&gt;&lt;a href=&quot;https://shortcoding.tistory.com/583&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://shortcoding.tistory.com/583&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ZCjGw/hyZrlYMf6F/GKtkUrLt6G4xXM1j48eGGK/img.png?width=800&amp;amp;height=439&amp;amp;face=0_0_800_439,https://scrap.kakaocdn.net/dn/MXhU9/hyZuGmVCTP/hZBrrkkU6eKkKEn12qCJhk/img.png?width=800&amp;amp;height=439&amp;amp;face=0_0_800_439,https://scrap.kakaocdn.net/dn/bKhzGw/hyZuxXQeOQ/Myzffk6msQZSgEwIJpRY5K/img.png?width=1918&amp;amp;height=1054&amp;amp;face=0_0_1918_1054');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;FE 아키텍처 변경과 이유 with 셸 앱(Sheel App)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 Vue3와 Next를 같이 사용해서 프론트엔드 MSA를 구현하려고 했다.사실 Next로 프로젝트를 하는 것이 주 목표였고, Vue3는 부가적으로 가져가는 것이 목표였다.백엔드를 다 만들고 나서 프론&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;shortcoding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>MSA 공부</category>
      <category>MSA</category>
      <author>5_hyun</author>
      <guid isPermaLink="true">https://shortcoding.tistory.com/579</guid>
      <comments>https://shortcoding.tistory.com/579#entry579comment</comments>
      <pubDate>Sat, 21 Jun 2025 18:02:09 +0900</pubDate>
    </item>
    <item>
      <title>이미지 레지스트리</title>
      <link>https://shortcoding.tistory.com/578</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이미지 레지스트리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 레지스트리는 &lt;b&gt;이미지를 저장하기 위한 저장소&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;레지스트리에서 이미지를 저장하고 공유 가능&lt;/li&gt;
&lt;li&gt;대표적인 레지스트리는 &lt;b&gt;Dcoker Hub&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-제공하는 기능&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 공유&amp;nbsp;&lt;/li&gt;
&lt;li&gt;이미지 검색&lt;/li&gt;
&lt;li&gt;이미지 버전 관리&lt;/li&gt;
&lt;li&gt;보안&lt;/li&gt;
&lt;li&gt;파이프라인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-이미지가 저장되는 공간&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호스트 머신의 로컬 스토리지&lt;/li&gt;
&lt;li&gt;퍼블릭 레지스트리&lt;/li&gt;
&lt;li&gt;프라이빗 레지스트리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지가 로컬 스토리지에 있으면 그걸 실행하고, 없으면 퍼블릭이나 프라이빗 레지스트리에서 다운 받아 컨테이너로 실행&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;-Docker Hub 말고 나만의 레지스트리를 사용하는 법&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에 레지스트리를 설치해서 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HARBOR&lt;/li&gt;
&lt;li&gt;Docker 프라이빗 레지스트리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;퍼블릭 클라우드의 서비스를 사용하는 방법
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS의 ECR&lt;/li&gt;
&lt;li&gt;Azure의 Container Registry(ACR)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;-이미지명 규칙&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;이미지 이름에는 이미지를 어디서 다운 받았는지, 어떤 버전을 받았는지에 대한 정보가 포함되어 있어야 함&amp;nbsp;&lt;/li&gt;
&lt;li&gt;하지만 &quot;docker run -d -p 80:80 --name hellonginx nginx&quot;로 해도 다운로드가 가능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이유는 이미지 이름에 규칙이 있고 디폴트로 지정되는 규칙이 있기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/paZ3P/btsOlgtIyoH/9sdIYK4nd2FZ8Xm9majy60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/paZ3P/btsOlgtIyoH/9sdIYK4nd2FZ8Xm9majy60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/paZ3P/btsOlgtIyoH/9sdIYK4nd2FZ8Xm9majy60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpaZ3P%2FbtsOlgtIyoH%2F9sdIYK4nd2FZ8Xm9majy60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1516&quot; height=&quot;100&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1516&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;/figure&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 레지스트리를 사용할지 지정&lt;/li&gt;
&lt;li&gt;Docker Hub 말고 다른 레지스트를 사용할 수 있기 때문&lt;/li&gt;
&lt;li&gt;이 주소가 비어있으면 Docker Hub의 주소인 &lt;b&gt;docker.io&lt;/b&gt;가 기본 주소로 되어 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;프로젝트명
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지를 보관하는 폴더 개념(레지스트리 마다 정의하는 개념이 다를 수 있음)&lt;/li&gt;
&lt;li&gt;Docker Hub에서는 가입한 사용자의 계정명이 프로젝트 명&lt;/li&gt;
&lt;li&gt;프로젝트명을 생략 가능한 이미지는 도커사가 직접 검증한 이미지여야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우에 이미지에 &quot;DOCKER OFFICAIAL IMAGE&quot; 라벨이 붙어 있고&lt;/li&gt;
&lt;li&gt;library라는 프로젝트에서 관리한다.&lt;/li&gt;
&lt;li&gt;따라서 프로젝트명의 기본값은 &lt;b&gt;library&lt;/b&gt;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;nginx는 도커에서 인증했기 때문에 프로젝트명을 입력하지 않아도 library가 기본으로 들어간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이미지명
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다운로드 받을 이미지의 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이미지 태그
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지의 버전&lt;/li&gt;
&lt;li&gt;숫자와 영문 모두 사용 가능&lt;/li&gt;
&lt;li&gt;비어있으면 &lt;b&gt;latest&lt;/b&gt;가 기본값&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-이미지 라벨 의미&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Docker Officaial Image&quot;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;217&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dErP2B/btsOkPDrJgA/GE3HkTt3jB2CBYv7K0p3s1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dErP2B/btsOkPDrJgA/GE3HkTt3jB2CBYv7K0p3s1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dErP2B/btsOkPDrJgA/GE3HkTt3jB2CBYv7K0p3s1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdErP2B%2FbtsOkPDrJgA%2FGE3HkTt3jB2CBYv7K0p3s1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;291&quot; height=&quot;217&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;217&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커에서 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Verified Publisher&quot;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Rb1Uk/btsOmXGqLH3/JYOaLrkl8kkqUClUuTKBoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Rb1Uk/btsOmXGqLH3/JYOaLrkl8kkqUClUuTKBoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rb1Uk/btsOmXGqLH3/JYOaLrkl8kkqUClUuTKBoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRb1Uk%2FbtsOmXGqLH3%2FJYOaLrkl8kkqUClUuTKBoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;503&quot; height=&quot;201&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;201&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 이미지는 아니지만 어느 정도 신뢰가 있는 회사에서 관리하는 것이라 다른 이미지보다 더 신뢰할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/courses/lecture?courseId=332726&amp;amp;type=LECTURE&amp;amp;unitId=199036&amp;amp;subtitleLanguage=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.inflearn.com/courses/lecture?courseId=332726&amp;amp;type=LECTURE&amp;amp;unitId=199036&amp;amp;subtitleLanguage=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1748697528091&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;이미지 레지스트리 실습 | 개발자를 위한 쉬운 도커&quot; data-og-description=&quot;이미지 레지스트리 실습&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/courses/lecture?courseId=332726&amp;amp;type=LECTURE&amp;amp;unitId=199036&amp;amp;subtitleLanguage=ko&quot; data-og-url=&quot;https://www.inflearn.com/courses/lecture?courseId=332726&amp;amp;unitId=199036&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oKYFc/hyY1fDyu5O/S1I3TCuA58qNkFVAidGSuK/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/x46TP/hyY0rkdsMS/kaWjJNlH86zlybmNH333L0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/courses/lecture?courseId=332726&amp;amp;type=LECTURE&amp;amp;unitId=199036&amp;amp;subtitleLanguage=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/courses/lecture?courseId=332726&amp;amp;type=LECTURE&amp;amp;unitId=199036&amp;amp;subtitleLanguage=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oKYFc/hyY1fDyu5O/S1I3TCuA58qNkFVAidGSuK/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/x46TP/hyY0rkdsMS/kaWjJNlH86zlybmNH333L0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;이미지 레지스트리 실습 | 개발자를 위한 쉬운 도커&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이미지 레지스트리 실습&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>인프런, 유데미/개발자를 위한 쉬운 도커</category>
      <category>Docker Hub</category>
      <category>이미지 레지스트리</category>
      <author>5_hyun</author>
      <guid isPermaLink="true">https://shortcoding.tistory.com/578</guid>
      <comments>https://shortcoding.tistory.com/578#entry578comment</comments>
      <pubDate>Sat, 31 May 2025 22:19:15 +0900</pubDate>
    </item>
    <item>
      <title>이미지와 컨테이너의 라이프사이클</title>
      <link>https://shortcoding.tistory.com/577</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이미지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소프트웨어의 실행법&lt;/b&gt;부터 알아보자&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;하드웨어 자원을 이용할 수 있게 해주는 &lt;b&gt;OS&lt;/b&gt;가 필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라이브러리&lt;/b&gt;나 &lt;b&gt;패키지&lt;/b&gt;에 의존하여 실행 혹은 &lt;b&gt;런타임&lt;/b&gt; 언어가 필요&lt;/li&gt;
&lt;li&gt;개발자가 개발한 &lt;b&gt;애플리케이션&lt;/b&gt;이나 다운로드 받은 &lt;b&gt;소프트웨어&lt;/b&gt;가 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 도커에서는 이런 과정 없이 이미지를 실행함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 가능한걸까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;예를 들어 Nginx 이미지를 실행하면, &lt;b&gt;이미지를 통해서 컨테이너를 실행&lt;/b&gt;했기 때문&lt;/li&gt;
&lt;li&gt;&lt;u&gt;&lt;b&gt;이미지는 파일 시스템의 특점 시점을 저장해 놓은 압축 파일임&lt;/b&gt;&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;이미지는 소프트웨어가 실행되기 위해 필요한 모든 것들을 미리 준비해서 압축&lt;/li&gt;
&lt;li&gt;따라서 이미지를 다운 받아서 OS의 격리된 공간에서 컨테이너가 실행되는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-이미지의 특성&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지는 운영 체제의 &lt;b&gt;백업 기능(스냅샷)&lt;/b&gt;과 유사하다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;언제든지 해당 시점의 소프트웨어를 실행할 수 있기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;하지만 이미지는 백업 기능이나 스냅샷보다 &lt;b&gt;크기가 작음&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;따라서 인터넷을 통해서 저장하고 공유하기 수월하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이미지와 컨테이너&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;이미지는 프로그램, 컨테이너는 프로세스라고 생각하면 된다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;u&gt;&lt;b&gt;이미지는 디스크를 차지, 컨테이너는 메모리와 CPU를 점유&lt;/b&gt;&lt;/u&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;격리된 공간에서 이미지에서 사전에 지정해 둔 프로그램이 프로세스로 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;하나의 이미지로 여러 개의 컨테이너 실행 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이미지와 메타데이터&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;u&gt;&lt;b&gt;이미지 메타데이터는 이미지가 실제로 압축된 데이터&lt;/b&gt;&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;메타데이터는 이미지의 데이터를 기록하는 데이터(이미지의 이름, 아이디, 사이즈 등등)&lt;/li&gt;
&lt;li&gt;메타데이터에는 &lt;b&gt;env&lt;/b&gt;, &lt;b&gt;cmd&lt;/b&gt;를 주로 봐야 함,
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;env&lt;/b&gt;는 애플리케이션이 사용하는 &lt;b&gt;환경 설정 값&lt;/b&gt;이며 key=value 형식, 소프트웨어 정보나 프로그램을 실행할 때 필요한 파일 경로 같은 정보가 포함, 즉 &lt;b&gt;소프트웨어가 사용할 설정 정보&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;cmd&lt;/b&gt;는 이미지를 컨테이너로 실행 시 &lt;b&gt;명령어&lt;/b&gt;를 지정할 수 있다. 이미지를 컨테이너로 실행할 때 CMD에 있는 명령어를 통해서 어떤 &lt;b&gt;프로그램을 실행할지를 메타데이터에서 결정&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;즉, 이미지의 압축 파일과 메타데이터를 사용해서 컨테이너가 만들어짐 &lt;/li&gt;
&lt;li&gt;메타데이터는 컨테이너를 &lt;b&gt;실행할 때 새로운 값으로 덮어쓸 수 있음&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;따라서 이미지 안에 있는 다른 프로그램을 실행할 수도 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨테이너의 라이프사이클&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2040&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6zWmi/btsOi7pEG42/gmTwzY0YnmkkUvY0uR4Euk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6zWmi/btsOi7pEG42/gmTwzY0YnmkkUvY0uR4Euk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6zWmi/btsOi7pEG42/gmTwzY0YnmkkUvY0uR4Euk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6zWmi%2FbtsOi7pEG42%2FgmTwzY0YnmkkUvY0uR4Euk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2040&quot; height=&quot;762&quot; data-origin-width=&quot;2040&quot; data-origin-height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;create
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이미지를 컨테이너로 만들 수 있음&lt;/li&gt;
&lt;li&gt;컨테이너를 실행하기 위한 격리된 공간이 만들어지는 상태&lt;/li&gt;
&lt;li&gt;네트워크, 스토리지, 환경 변수 같은 모든 리소스가 격리된 공간인 컨테이너로 분리된 상태&lt;/li&gt;
&lt;li&gt;하지만 생성 단계에서는 내부에서 프로세스를 실제로 실행하지 않기 때문에 &lt;b&gt;호스트 OS의 CPU와 메모리를 사용하지 않음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;컨테이너&amp;nbsp; 메타데이터의 cmd와 env 명령어를 사용해서 컨테이너를 러닝 상태로 만들 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;running
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;컨테이너에서 프로세스가 실행 중이라는 것을 의미&lt;/li&gt;
&lt;li&gt;&lt;b&gt;호스트 OS의 CPU와 메모리를 사용&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;실행 중인 프로세스에 종료나 재시작 신호를 보내면 보내면 10초 뒤에 명령 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;paused
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;컨테이너에서 실행 중인 모든 프로세스가 일시 중지된 상태&lt;/li&gt;
&lt;li&gt;일시 중지한다는 것은 현재의 상태를 모두 메모리에 저장해 두는 것&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CPU는 사용하지 않고, 메모리만 사용&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;unpause 명령어를 사용하면 프로세스를 일시 정지한 시점부터 재시작 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;stopped
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;컨테이너에서 실행 중인 프로세스를 완전히 중단시켰다는 것을 의미&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메모리와 CPU 사용이 모두 중단&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;종료된 컨테이너를 다시 시작하면 프로세스가 처음부터 다시 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;deleted
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;rm 명령어를 이용해서 삭제 가능&lt;/li&gt;
&lt;li&gt;실행 중인 컨테이너를 삭제하려면 -f 옵션을 사용해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-출처&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/courses/lecture?courseId=332726&amp;amp;type=LECTURE&amp;amp;unitId=199036&amp;amp;subtitleLanguage=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.inflearn.com/courses/lecture?courseId=332726&amp;amp;type=LECTURE&amp;amp;unitId=199036&amp;amp;subtitleLanguage=ko&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1748697567873&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;이미지 레지스트리 실습 | 개발자를 위한 쉬운 도커&quot; data-og-description=&quot;이미지 레지스트리 실습&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/courses/lecture?courseId=332726&amp;amp;type=LECTURE&amp;amp;unitId=199036&amp;amp;subtitleLanguage=ko&quot; data-og-url=&quot;https://www.inflearn.com/courses/lecture?courseId=332726&amp;amp;unitId=199036&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oKYFc/hyY1fDyu5O/S1I3TCuA58qNkFVAidGSuK/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/x46TP/hyY0rkdsMS/kaWjJNlH86zlybmNH333L0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/courses/lecture?courseId=332726&amp;amp;type=LECTURE&amp;amp;unitId=199036&amp;amp;subtitleLanguage=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/courses/lecture?courseId=332726&amp;amp;type=LECTURE&amp;amp;unitId=199036&amp;amp;subtitleLanguage=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oKYFc/hyY1fDyu5O/S1I3TCuA58qNkFVAidGSuK/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/x46TP/hyY0rkdsMS/kaWjJNlH86zlybmNH333L0/img.png?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;이미지 레지스트리 실습 | 개발자를 위한 쉬운 도커&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이미지 레지스트리 실습&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>인프런, 유데미/개발자를 위한 쉬운 도커</category>
      <category>라이프사이클</category>
      <category>이미지</category>
      <category>컨테이너</category>
      <author>5_hyun</author>
      <guid isPermaLink="true">https://shortcoding.tistory.com/577</guid>
      <comments>https://shortcoding.tistory.com/577#entry577comment</comments>
      <pubDate>Sat, 31 May 2025 21:40:03 +0900</pubDate>
    </item>
  </channel>
</rss>