← Back to list

모노레포가 미리캔버스 프론트엔드 아키텍처를 뒷받침 하는 방법 [2/2]

1편에서는 모노레포의 개념들과 미리캔버스가 모노레포를 구성하는 방법에대해서 간단하게 살펴보았습니다.

종현 김 in 미리디 블로그 · 2026-06-23 01:59 · 1 claps · 21.2 min read
#monorepo #micro-front-end #frontend #front-end-development #tech
Open on Medium ↗
Wiki topics: 🌐 · Web Development

모노레포가 미리캔버스 프론트엔드 아키텍처를 뒷받침 하는 방법 [2/2]

1편에서는 모노레포의 개념들과 미리캔버스가 모노레포를 구성하는 방법에대해서 간단하게 살펴보았습니다.

2편에서는 실제로 미리캔버스의 모노레포가 어떻게 동작하는지, 그리고 모노레포가 동작하는 플랫폼이 서비스 아키텍처를 어떻게 뒷받침하고 있는지 살펴보려고 합니다. CI/CD의 흐름부터 플랫폼의 관점에서 모노레포를 이해하는방법까지 살펴보려고 합니다.

CI의 아키텍처 세 개의 파이프라인이 독립적으로 동작하는 이유

아키텍처 규칙이 코드 수준에서 강제된다면, 그 다음은 이 코드를 안전하게 검증하는 CI 파이프라인입니다. 미리캔버스의 CI는 세 개의 독립 파이프라인으로 구성됩니다.

  • lint: biome, eslint
  • type-check: tsc
  • test: vitest, RTL (React Testing Library)

여기서 중요한 것은 이 세 파이프라인이 서로 독립적으로 병렬 실행된다는 점입니다. Lint가 실패해도 Type-check와 Test는 계속 돌아갑니다. 개발자는 PR 하나로 세 가지 검증 결과를 동시에 확인할 수 있습니다.

왜 독립적으로 실행이 가능한가? 그 비밀은 각 도구가 소비하는 아티팩트(artifact)의 차이에 있습니다.

lint -> 의존성이 필요 없는 파이프라인

biome이나 eslint는 소스 파일의 AST(Abstract Syntax Tree)를 분석합니다. 코드를 실행하거나 컴파일하지 않습니다. 단순히 텍스트를 파싱해서 우리가 정의한 규칙에 맞는지 확인합니다.

따라서 하위 패키지들이 빌드되어 있을 필요가 없습니다. 하위 의존성의 타입 선언도 필요하지 않습니다. entity-user 패키지가 있든 없든, lint는 feature-auth 소스 파일만 보고 린팅 규칙을 검사할 수 있습니다.

한 가지 주의할 점이 있습니다. linter를 통해 타입을 체크하는 플러그인(예: @typescript-eslint/no-unsafe-assignment)을 구성하면, lint도 tsc의 결과에 의존하게 됩니다. 미리캔버스에서는 의도적으로 linter에서 타입 검사를 하지 않습니다. 타입 검사는 Type-check 파이프라인이 담당하도록 역할을 분리한 것입니다.

type-check -> .d.ts만 필요한 파이프라인

tsc를 이용한 타입 체크는 하위 의존성의 타입 정보가 필요합니다. feature-auth가 entity-user의 User 타입을 사용하고 있다면, tsc는 User가 어떻게 생겼는지 알아야 타입 체크를 수행할 수 있습니다.

이때 중요한 것은 tsc가 .d.ts 타입 선언 파일만 있으면 된다는 점입니다. 실행 가능한 JavaScript 번들(.js)은 필요 없습니다. 타입 체크는 코드를 실행하는 것이 아니라 타입 구조를 분석하는 것이기 때문입니다.

TypeScript의 emitDeclarationOnly: true 컴파일러 옵션이 이것을 가능하게 합니다. 이 옵션을 사용하면 tsc가 .js 파일은 생성하지 않고 .d.ts 타입 선언만 출력합니다. Type-check 파이프라인이 번들러 파이프라인과 완전히 분리될 수 있는 이유입니다.

// packages/entity-user/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationDir": "./dist/types",
    "emitDeclarationOnly": true,
    "incremental": true,
    "tsBuildInfoFile": "./.tsbuildinfo"
  }
}

composite: true와 incremental: true를 함께 쓰면 .tsbuildinfo 파일에 이전 빌드 결과가 캐시됩니다. 변경된 파일만 다시 분석하므로, 패키지 수가 많아져도 타입 체크가 점진적으로 빨라집니다.

test -> 번들러 결과가 필요한 파이프라인

테스트는 코드를 실제로 실행합니다. vitest가 feature-auth의 테스트를 돌리려면, entity-user 패키지가 실제로 실행 가능한 JavaScript 모듈이어야 합니다. 번들러가 컴파일한 .js 결과물이 필요한 것입니다.

반대로 .d.ts 타입 선언은 테스트 실행에 필요하지 않습니다. 타입 정보는 TypeScript 컴파일 단계에서만 사용되고, 런타임에는 존재하지 않습니다.

따라서 테스트 파이프라인의 의존 관계는 다음과 같습니다.

테스트 파이프라인
├── 소스 파일 (.ts, .tsx) 필요
└── 하위 패키지의 .js 번들 필요 (번들러 선행 필요)

─── 하위 패키지의 .d.ts 불필요

번들러가 .d.ts를 생성하지 않도록 설정하면, Type-check 파이프라인과 Test 파이프라인이 각자의 아티팩트를 독립적으로 생성합니다. 두 파이프라인은 서로의 결과를 기다릴 필요가 없습니다.

세 파이프라인의 독립성을 정리하면 다음과 같습니다.

이 세 파이프라인은 서로의 원인도 결과도 아닙니다. 병렬로 실행되어 피드백 루프를 최대한 빠르게 만듭니다. PR을 올리면 한 번의 파이프라인 실행으로 세 가지 검증이 동시에 이루어지고, 개발자는 어떤 파이프라인이 실패했는지 즉시 확인할 수 있습니다.

CD — 두 가지 모듈 통합 방식

코드가 CI를 통과했다면 다음은 배포(CD)입니다. 미리캔버스는 두 가지 방식으로 모듈을 통합합니다. 이 두 방식의 차이를 이해하는 것이 아키텍처의 핵심 중 하나입니다.

Build-time 통합: 번들러가 하나로 묶는 방식

Build-time 통합은 직관적입니다. @miridih/entity-user 와 @miridih/shared-utils 를 의존하는 @miridih/feature-signin패키지는 pnpm workspace에서 “@miridih/entity-user”: “workspace:*”로 참조하면, 번들러(Vite, Webpack, Rollup)가 빌드 시점에 두 패키지의 코드를 분석하고 하나의 번들로 합칩니다.

pnpm이 package.json의 의존성을 해석하는 과정도 중요합니다.

pnpm은 모듈 해석과정에서 파일을 content-addressable store에 저장하고, 프로젝트의 node_modules/.pnpm 위치의 virtual store에는 이를 hardlink로 배치하며, 실제 의존성 그래프는 symlink를 통해 구성합니다.

workspace 패키지에 workspace:* 프로토콜을 사용하면 개발중에는 반드시 workspace패키지를 symlink형태로 참조하며 npm registry에 publish 또는 pack 할때는 해당 workspace의 실제버전으로 자동 변환됩니다.

하고, 패키지를 npm에 퍼블리시할 때는 실제 버전 번호로 자동 변환됩니다.

// packages/feature-signin/package.json
{
  "name": "@miridih/feature-signin",
  "dependencies": {
    "@miridih/entity-user": "workspace:*",
    "@miridih/shared-utils": "workspace:*"
  }
}

Build-time 통합의 장점은 명확합니다.

  • 예측 가능성: 빌드된 시점에 어떤 코드가 사용되는지 모두 확정됩니다
  • 정적 분석 용이: 번들 결과물을 분석하면 전체 의존성이 보입니다
  • 디버깅 단순화: 런타임에 어떤 코드가 실행되는지 예측하기 쉽습니다
  • 번들 최적화: 빌드 시점에 Tree Shaking, Code Splitting 등 다양한 최적화를 적용할 수 있습니다

단점도 분명합니다. 하위 패키지가 변경되면 그 패키지를 직접 또는 간접으로 의존하는 모든 앱을 재빌드하고 재배포해야 합니다. 미리캔버스처럼 entity-user 패키지가 수십 개 앱에서 사용된다면, 이 패키지 하나의 변경이 수십 번의 재배포를 유발합니다.

미리캔버스에서는 shared, entity, feature, service 계층이 Build-time 통합을 사용합니다. 이 계층들은 변화율이 낮고, 주로 로직과 타입을 담당하며, UI와 직접 관련이 없습니다. 재배포 빈도가 낮으므로 Build-time 통합의 단점이 크게 드러나지 않습니다.

Run-time 통합: 실행 중에 조립하는 방식

Run-time 통합은 완전히 다른 세계입니다. 각 모듈이 독립적으로 번들되어 CDN에 배포됩니다. Host(Shell) 앱은 실행 시점에 다른 모듈을 동적으로 로드합니다. Run-time에 통합되는 모듈이 변경되었을 때 Host 앱을 재빌드하거나 재배포하지 않아도 됩니다.

미리캔버스에서는 fragment 계층부터 Run-time 통합을 지원합니다. Fragment는 use-case 단위의 비즈니스 로직과 UI가 결합된 공간으로, 사용자의 특정 인터랙션을 통해 하나의 비즈니스 플로우를 완결할 수 있는 단위입니다. “편집 영역 Fragment”, “결제 Fragment”, “로그인 Fragment” 같은 것들이 각각 독립적으로 개발되고 배포됩니다.

Host 앱은 실행 시 manifest.json을 fetch해서 어떤 Fragment가 어디 있는지 알아냅니다. Fragment 팀이 새 버전을 배포하면 manifest.json만 업데이트하면 됩니다. Host 앱은 다음 페이지 로드 시 자동으로 새 버전을 사용합니다.

이것이 진정한 독립 배포(Independent Deployment)입니다. Fragment 팀이 Host 앱 팀에게 배포를 기다려달라고 할 필요가 없습니다. 자신의 Fragment를 언제든지 독립적으로 배포할 수 있습니다.

Module Federation이 모듈을 어떻게 런타임에 통합해줄 수 있나요?

Run-time 통합을 가능하게 하는 기술이 Webpack 5 Module Federation입니다. 2020년 Zack Jackson이 발표한 이 기술은 브라우저에서 JavaScript 모듈을 런타임에 동적으로 공유하는 메커니즘을 제공합니다.

Module Federation 이전에는 어떤 방법들이 있었을까요? 역사를 잠깐 살펴보면 왜 Module Federation이 중요한지 더 잘 이해할 수 있습니다.

Container: remoteEntry.js의 역할

Webpack 5 Module Federation으로 빌드된 각 Remote는 remoteEntry.js라는 진입점 파일을 생성합니다. 이 파일이 바로 컨테이너(Container)입니다.

컨테이너는 두 가지 핵심 API를 노출합니다.

// remoteEntry.js가 노출하는 인터페이스
container = {
  init(shareScope) {
    // Host가 준비한 shareScope를 Remote에 주입
    // React는 이 scope에 자신의 shared를 등록
  },
  get(moduleName) {
    // 특정 모듈을 비동기로 반환
    // './EditorFragment', './EditorToolbar' 등
  }
}

Host 앱은 부팅 단계에서 shareScope에 자신의 shared 모듈을 미리 등록해두고, Remote의 remoteEntry.js를 동적으로 로드합니다. 로드된 container의 init(shareScope)로 준비된 shareScope를 Remote에 넘겨 Remote 쪽 shared를 등록한 뒤, get(‘./EditorFragment’)로 실제 컴포넌트를 가져옵니다.

Shared Scope: 공유 의존성의 런타임 협상

Module Federation에서 가장 정교한 부분이 바로 공유 스코프(Shared Scope)입니다. Host 앱과 여러 Fragment가 모두 React를 사용한다면, 각자 React를 번들에 포함시키면 메모리에 React가 여러 개 로드됩니다. 이것은 단순히 용량 낭비가 아니라 런타임 오류를 유발합니다.

React의 Hook은 같은 React 인스턴스의 내부 상태를 공유합니다. Host 앱의 React와 Fragment의 React가 다른 인스턴스라면, Hook이 다른 컨텍스트에서 실행되어 Cannot read properties of null (reading ‘useState’) 같은 오류가 발생합니다.

공유 스코프는 이 문제를 런타임 버전 협상으로 해결합니다. 동작 방식은 다음과 같습니다.

  1. Host 앱이 webpack_share_scopes.default를 초기화합니다
  2. 각 Fragment의 init()이 호출될 때 제공 가능한 React 버전을 등록합니다
  3. 모듈 로드 시점에 semver 규칙으로 가장 적합한 버전을 선택합니다
  4. singleton: true 이면 공유 스코프 내에서 단일 버전만 사용하며, 버전 충돌 시 strictVersion 설정에 따라 경고 또는 에러를 발생시킵니다.
// webpack.config.js (Fragment)
new ModuleFederationPlugin({
  name: "editor_fragment",
  filename: "remoteEntry.js",
  exposes: {
    "./EditorFragment": "./src/EditorFragment",
    "./EditorToolbar": "./src/EditorToolbar",
  },
  shared: {
    react: {
      singleton: true,        // 하나의 인스턴스만 허용
      requiredVersion: "^18.0.0",
    },
    "react-dom": {
      singleton: true,
      requiredVersion: "^18.0.0"
    }
  }
})
// webpack.config.js (Host App)
new ModuleFederationPlugin({
  name: "main_app",
  remotes: {
    editor_fragment: "editor_fragment@https://cdn.miridih.com/fragments/editor/remoteEntry.js"
  },
  shared: {
    react: {
      singleton: true,
    },
    "react-dom": { singleton: true, eager: true }
  }
})

manifest.json 기반 동적 로딩이 진정한 독립 배포인 이유

기존 Module Federation 설정에서는 Fragment의 URL이 Host 앱의 webpack.config.js에 하드코딩됩니다.

// ❌ URL 하드코딩 방식
remotes: {
  editor_fragment: "editor_fragment@https://cdn.miridih.com/editor/remoteEntry.js"
}

이렇게 하면 Fragment 팀이 새 버전을 배포할 때 URL이 바뀌면 Host 앱도 재빌드해야 합니다. 독립 배포의 의미가 퇴색됩니다.

manifest.json 기반 방식은 이 문제를 해결합니다. Host 앱은 Fragment의 실제 URL을 모르고, manifest.json의 위치만 압니다. 런타임에 manifest.json을 fetch해서 현재 배포된 버전의 실제 URL을 알아냅니다.

플랫폼 엔지니어링으로 모노레포를 건강하게 유지하는 방법

CI/CD가 갖춰지면 다음 과제는 옵저버빌리티(Observability)입니다. 모노레포가 수십~수백 개 패키지 규모로 성장하면, 사람이 일일이 상태를 확인하고 관리하는 것은 불가능합니다. 공유하기 쉬운 구조를 가져가는 대신 아키텍처의 복잡도는 높아집니다. 결국 이 복잡도는 도구로 관리해야 합니다.

미리캔버스의 플랫폼 팀은 레포지토리에서 발생하는 중요한 사항들을 측정하고 분석합니다. 크게 세 가지 영역입니다.

1. 의존성 가시화

모노레포 내에 존재하는 패키지가 20~30개 수준이라면 변경사항의 전파를 확인하고 관리하는 것이 어렵지 않습니다. 하지만 수백 개, 수천 개 수준에서는 하나의 모듈 변경이 발생시키는 전파 범위를 추적하기 어렵습니다.

핵심은 두 지표입니다.

Fan-in (진입 차수): 특정 패키지를 참조하는 다른 패키지의 수입니다. Fan-in이 높은 패키지는 변경의 전파 범위가 넓습니다.

shared-utils 패키지의 fan-in이 50이라면, 이 패키지를 수정할 때 최대 50개 패키지의 재빌드가 발생합니다. Turborepo나 Nx의 affected 명령이 이것을 자동으로 계산해주지만, fan-in 지표 자체를 추적하면 “이 패키지는 변경할 때 특히 조심해야 한다”는 사실을 팀이 인식할 수 있습니다.

Fan-out (진출 차수): 특정 패키지가 참조하는 다른 패키지의 수입니다. Fan-out이 비정상적으로 높은 패키지는 지나치게 많은 역할을 수행하고 있다는 신호입니다. 모듈 분할이 필요할 수도 있습니다.

CI 파이프라인에서 fan-in 임계값을 초과하면 PR에 자동으로 경고를 남기도록 설정할 수 있습니다.

# GitHub Action에서 fan-in 측정
pnpx depcruise --output-type json packages/shared/src/index.ts \
  | jq '[.modules[].dependents | length] | max'

manifest 기반으로 런타임에 통합되는 Fragment 계층은 정적 의존성 분석만으로는 전체 그림을 파악하기 어렵습니다. 정적 의존성 그래프에서는 Host 앱이 Fragment를 직접 참조하지 않기 때문입니다. 미리캔버스는 manifest 파일들의 변경 이력을 별도로 추적하고, E2E 테스트에서 번들 로딩 추적을 통해 런타임 의존성도 분석할 수 있도록 구성하고 있습니다.

2. CI/CD 병목 관찰

거대한 모노레포의 CI는 아무런 조치 없이 두면 서서히 느려집니다. 한 번에 폭발하지 않고 조금씩 누적되기 때문에 느려지는 지점을 찾기 어렵습니다.

추적해야 하는 주요 지표들입니다.

특히 캐시 무효화 전파(Cache Invalidation Cascade)는 가장 조용하고 위험한 성능 킬러입니다. entity 계층 패키지 하나가 변경되면, 이를 직접 또는 간접으로 의존하는 feature, service, fragment 계층 전체의 캐시가 무효화됩니다.

Turborepo의 캐시 키는 turbo.json의 inputs에 지정된 파일들의 해시 합산입니다. inputs를 느슨하게 설정하면 불필요한 캐시 무효화가 발생합니다.

// turbo.json - inputs를 정밀하게 설정
{
  "pipeline": {
    "lint": {
      "inputs": ["src/**/*.{ts,tsx}", ".eslintrc.js", "biome.json"],
      "outputs": []
    },
    "typecheck": {
      "inputs": ["src/**/*.{ts,tsx}", "tsconfig.json", "../../tsconfig.base.json"],
      "outputs": [".tsbuildinfo", "dist/types/**/*.d.ts"],
      "dependsOn": ["^typecheck"]
    },
    "test": {
      "inputs": ["src/**/*.{ts,tsx,test.ts,test.tsx}", "vitest.config.ts"],
      "outputs": ["coverage/**"],
      "dependsOn": ["^build"]
    },
    "build": {
      "inputs": ["src/**/*.{ts,tsx}", "package.json", "vite.config.ts"],
      "outputs": ["dist/**", "!dist/types/**"],
      "dependsOn": ["^build"]
    }
  }
}

package.json 전체를 inputs에 넣으면 description 필드 변경만으로도 캐시가 깨집니다. 실제로 빌드 결과에 영향을 주는 dependencies 변경만 캐시 키에 반영되도록 정밀하게 설정해야 합니다.

또한 pnpm-lock.yaml이 변경되면 외부 패키지 버전 업그레이드를 의미하므로, 대부분의 도구가 전체 캐시를 무효화합니다. 외부 패키지 업그레이드는 CI 부하가 크다는 점을 팀이 인식해야 합니다. 주기적으로 일괄 업그레이드하는 것이 매번 조금씩 업그레이드하는 것보다 CI 비용 면에서 효율적일 수 있습니다.

3. 패키지 건강 지표 자동화

패키지 자체의 건강 상태도 지속적으로 모니터링해야 합니다.

순환 의존(Circular Dependency) 감지

순환 의존은 계층 경계가 무너지기 시작할 때 가장 먼저 나타나는 신호입니다. 알고리즘적으로는 Tarjan의 강결합요소(Strongly Connected Components) 알고리즘이 사용됩니다. O(V+E) 시간 복잡도로 단일 DFS 순회만으로 모든 순환을 찾아냅니다.

순환 의존이 생기면 두 가지 문제가 발생합니다. 첫째, affected 분석의 정확도가 저하됩니다. 둘째, 빌드 순서를 결정할 수 없게 됩니다(DAG가 아니므로 위상 정렬 불가).

madge — circular packages/를 pre-commit hook에 등록하면 로컬에서 즉시 감지할 수 있습니다.

레이어 규칙 위반 자동 감지

앞서 설명한 @nx/enforce-module-boundaries 또는 dependency-cruiser가 CI에서 이를 차단합니다. 순환 의존과 레이어 위반은 단 한 건도 허용해서는 안 됩니다. 경고(warning)가 아니라 에러(error)로 처리하고 PR 머지를 차단해야 합니다.

Conway’s Law와 프론트엔드 플랫폼 팀의 역할

1968년 Melvin Conway가 제안한 법칙이 있습니다.

역으로 생각하면, 원하는 소프트웨어 아키텍처가 있다면 그 아키텍처를 닮은 팀 구조를 먼저 만들어야 합니다. Martin Fowler는 이를 역 콘웨이 법칙(“Inverse Conway Maneuver”)이라고 불렀습니다.

미리캔버스의 레이어드 아키텍처는 팀의 독립성을 코드 구조로 구현하려는 시도입니다. Fragment 계층이 팀 단위의 자율 배포를 지원하는 것은 단순한 기술적 설계가 아니라, 팀이 독립적으로 일할 수 있는 구조를 코드 레벨에서 보장하려는 것입니다.

플랫폼 팀은 다른 팀들이 아키텍처 규칙을 직접 신경 쓰지 않아도 올바른 구조를 따를 수밖에 없도록 만드는 역할을 합니다. 규칙을 문서가 아닌 도구로 강제하고, 위반이 발생하면 즉시 피드백을 제공합니다.

모노레포의 구조가 마이크로 프론트엔드 아키텍처를 뒷받침하기

미리캔버스 프론트엔드 팀의 모노레포 여정을 통해 발견한 핵심 교훈은 이 한 문장입니다.

“아키텍처의 결정사항들과 모노레포의 관리는 플랫폼으로 관리하도록 구성하는 것이 핵심입니다.”

계층 규칙은 문서에 적는 것이 아니라 ESLint가 강제합니다. 순환 의존은 팀원이 코드 리뷰에서 잡는 것이 아니라 CI가 차단합니다. 느린 빌드는 직감으로 찾는 것이 아니라 지표가 보여줍니다. Fragment 배포는 다른 팀의 승인을 기다리는 것이 아니라 소유권을 가진 팀의 manifest.json 업데이트로 완결됩니다.

이 모든 것의 출발점은 “모노레포 = 독립적인 패키지들의 집합”이라는 정의를 진지하게 받아들이는 것입니다. 독립이란 물리적으로 같은 공간에 있으면서도 논리적으로는 서로에게 불필요한 영향을 주지 않는 것입니다. 그리고 그 독립을 유지하기 위한 규칙들이 사람의 기억과 의지에 의존하지 않고 도구에 의해 보장될 때, 아키텍처는 비로소 살아있을 수 있습니다.

거대한 모노레포를 모놀리스로 만드는 것도 사람이고, 수백 개의 독립 패키지들이 유기적으로 협력하는 시스템으로 만드는 것도 사람입니다. 그 과정에서 도구는 사람이 실수하지 않도록 도와주는 안전망입니다. 미리캔버스는 그 안전망을 플랫폼으로 정의하고 관리하고 있습니다.

참고자료

  • Cam Jackson, “Micro Frontends” (http://martinfowler.com , 2019) — Build-time/Run-time 통합 분류 체계의 출처
  • Robert C. Martin, “Clean Architecture” (2017) — Stable Dependencies Principle, 계층별 변화율 원칙의 이론적 근거
  • Luca Mezzalira, “Building Micro-Frontends” (O’Reilly, 2021) — Micro Frontend 계층형 설계의 실전 레퍼런스
  • Matthew Skelton & Manuel Pais, “Team Topologies” (2019) — Stream-aligned team, Platform team 역할 정의
  • Zack Jackson, Webpack 5 Module Federation 공식 문서 — Container, Shared Scope, manifest 기반 로딩 메커니즘
  • TypeScript 공식 문서, Project References — composite, emitDeclarationOnly, incremental 옵션
  • Martin Fowler, “Architecture Decision Record” — ADR 경량 문서화 방식

메타데이터
post_id
dd802a16e7a8
slug
모노레포가-미리캔버스-프론트엔드-아키텍처를-뒷받침-하는-방법-2-2-dd802a16e7a8
url
https://medium.com/miridih/%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EA%B0%80-%EB%AF%B8%EB%A6%AC%EC%BA%94%EB%B2%84%EC%8A%A4-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%A5%BC-%EB%92%B7%EB%B0%9B%EC%B9%A8-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-2-2-dd802a16e7a8
canonical_url
https://medium.com/miridih/%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EA%B0%80-%EB%AF%B8%EB%A6%AC%EC%BA%94%EB%B2%84%EC%8A%A4-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%A5%BC-%EB%92%B7%EB%B0%9B%EC%B9%A8-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-2-2-dd802a16e7a8
author_url
https://medium.com/@console_log
status
ok
fetched_at
2026-07-08 20:12:56