문서 읽는 데 47분 · H9

H-9: 마이크로 프론트엔드 — 화면을 여러 팀이 나눠 만들 때

목차 46
전체 59강 중 59강 · 리액트
난이도 · 중급선수지식HTML·CSS·JS

ℹ️TypeScript · React · Next.js 트랙이에요. html-css-js에서 만든 순수 JS 화면을 React 컴포넌트로 다시 짭니다. 웹 기초를 먼저 익히고 오면 좋아요.

안녕하세요, 홍순구 튜터입니다.

지난 시간 마무리에서 "다음 시간에는 화면을 여러 팀이 나눠 만드는 이야기로 넘어간다" 고 말씀드렸죠. 오늘이 그 시간이고, 동시에 이 과목의 마지막 시간입니다.

지금까지 우리는 인스타그램 클론 하나를 계속 키워왔습니다. 정적인 HTML 이었던 것이 React 컴포넌트가 되고, 라우터와 서버 상태가 붙고, Next.js로 서버까지 넘어갔고, 테스트와 인증이 얹혔고, 오류와 속도를 재는 눈이 생겼고, 번들을 들여다보고, 네 나라 말을 하게 됐는데, 이 모든 과정을 한 저장소에서 한 사람이 해왔다는 게 실무와는 다른 지점이에요.

피드 팀, 검색 팀, 메시지 팀이 따로 있고 각자 배포 주기가 다른데, 검색 팀이 버튼 색 하나 바꾸겠다고 앱 전체를 다시 빌드하고 다시 배포해야 한다면 어떨까요. 게다가 그 배포에 다른 두 팀의 미완성 코드까지 섞여 들어간다면요.

오늘 다룰 것은 그 문제를 푸는 방법이면서, 대부분의 경우 그 방법을 쓰지 말아야 하는 이유이기도 합니다. 오늘은 기술을 배우는 시간이면서, 배운 기술을 안 쓰기로 결정하는 연습을 하는 시간이에요.

텍스트
 오늘의 여정

 1. 원격 앱이 자기 코드를 내보낸다   remoteEntry.js 라는 이름표
 2. 호스트가 실행 중에 가져온다      디스크에 없는 파일을 import 한다
 3. 원격만 배포해도 호스트는 그대로   해시를 대조해서 확인한다
 4. React 를 두 번 싣지 않는 일      shared 는 최적화가 아니었다
 5. 그 대가를 잰다                   파일 1개  24개
 6. 경계를 넘는 약속은 조용히 깨진다  빌드는 초록인데 화면이 없다
 7. 그래서 우리 앱엔 안 넣습니다      쓸지 말지 이전에 쓸 수 있는지

💡 오늘 수업의 핵심 — "나눌 수 있다는 것과 나눠야 한다는 것은 다릅니다"


🎯 학습 목표

  • 따로 빌드된 앱의 코드를 실행 중에 가져와 화면에 그리고, 그것이 정말 따로 배포되는지 산출물 해시로 확인합니다.
  • 여러 앱이 React 같은 라이브러리를 나눠 쓰는 방식을 이해하고, 그 선언을 뺐을 때 무엇이 무너지는지 직접 봅니다.
  • 이 구조가 물리는 비용을 바이트와 파일 수로 재고, 어떤 규모에서 손해인지 나눗셈으로 판단합니다.
  • 검색으로 만나는 최신 정보가 실제로 존재하는지 확인하는 방법을 익힙니다.

Step 1: 원격 앱이 자기 코드를 내보낸다

나누고 싶어지는 순간

먼저 무엇을 나눌지 정해봅시다. 우리 앱에서 가장 독립적인 조각 하나를 고르면 좋아요 버튼인데, 좋아요는 눌리면 숫자가 올라가고 색이 바뀌는 게 전부라서 피드가 어떻게 생겼는지, 프로필이 뭘 보여주는지 몰라도 됩니다.

이 버튼을 다른 팀이 소유한 별개의 앱으로 떼어낸다고 해보면, 그 팀은 자기 저장소에서 자기 일정으로 배포하게 됩니다.

떼어낸 쪽을 원격(remote), 그걸 가져다 쓰는 쪽을 호스트(host)라고 부릅니다.

내보낼 것을 고른다

원격 앱부터 만듭니다. 평범한 Vite + React 앱인데 설정에 한 덩어리가 더 붙어요.

JavaScript
// scratch/h9-remote/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { federation } from '@module-federation/vite'

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'like_app',
      filename: 'remoteEntry.js',
      // 밖으로 내보낼 것만 여기 적는다. 안 적은 파일은 남이 못 가져간다.
      exposes: { './LikeButton': './src/LikeButton.jsx' },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
    }),
  ],
  build: { target: 'chrome89' },
})

exposes가 핵심입니다. 이 앱에는 파일이 여러 개 있지만 밖으로 나가는 것은 ./LikeButton 하나뿐이에요. 나머지는 이 앱 안에서만 씁니다.

shared는 지금은 그냥 두고 Step 4에서 정면으로 다룹니다.

내보내는 컴포넌트 자체는 특별할 게 없어요.

jsx
// scratch/h9-remote/src/LikeButton.jsx
import { useState } from 'react'

export default function LikeButton({ initialCount }) {
  const [liked, setLiked] = useState(false)
  const count = initialCount + (liked ? 1 : 0)

  return (
    <button aria-pressed={liked} onClick={() => setLiked((v) => !v)}>
      {liked ? '♥' : '♡'} 좋아요 {count.toLocaleString('ko-KR')}개
    </button>
  )
}

B-2에서 만든 좋아요 토글과 거의 같습니다. aria-pressed는 E-7에서 아이콘 버튼에 붙였던 것이고, 자릿점은 지난 시간에 배운 방식 그대로예요.

빌드하면 생기는 것

빌드하면 dist/에 평소 보던 것들과 함께 파일 하나가 더 나옵니다.

텍스트
 dist/remoteEntry.js    17.13 kB │ gzip: 5.74 kB

이게 이름표입니다. 이 앱이 무엇을 내보내는지, 어떤 라이브러리를 나눠 쓸 생각인지 적혀 있어요. 남의 앱은 이 파일 주소만 알면 됩니다.

🙋 학생 질문 — "17 kB 면 꽤 큰데요? 목록만 적힌 파일 아닌가요?"

목록만 있으면 훨씬 작은데, 실제로 이 실습을 React 없이 문자열 하나만 내보내게 만들어보면 remoteEntry.js가 140 B밖에 안 돼요.

17 kB로 불어난 이유는 shared 때문이에요. "나는 react를 나눠 쓸 건데, 버전은 이것이고, 조건은 이렇다"는 협상표가 함께 실리고, 협상을 실제로 진행하는 코드도 함께 필요하거든요.

목록이 아니라 협상 창구라고 보시는 게 맞아서, Step 4가 따로 있는 거예요.

💡 한 줄 정리

원격 앱은 exposes에 적은 것만 밖으로 내보내고, 무엇을 내보내는지는 remoteEntry.js라는 이름표에 적힌다.


Step 2: 호스트가 실행 중에 가져온다

디스크에 없는 파일을 import 한다

지금부터는 가져다 쓰는 쪽으로 넘어갑니다.

JavaScript
// scratch/h9-host/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { federation } from '@module-federation/vite'

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'feed_app',
      // 주소가 코드에 들어간다. 빌드할 때가 아니라 「실행할 때」 이 주소로 받으러 간다.
      remotes: {
        like_app: {
          type: 'module',
          name: 'like_app',
          entry: 'http://localhost:5301/remoteEntry.js',
        },
      },
      shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
    }),
  ],
  build: { target: 'chrome89' },
})

remotes에 이름과 주소를 적는데, 여기서 한 번 멈춰서 보실 게 있어요. 주소가 설정에 들어간다는 점이에요. 지금까지 우리가 import 하던 것은 전부 디스크에 있는 파일이었는데, 이번엔 아닙니다.

쓰는 쪽 코드를 보시면 더 분명해집니다.

jsx
// scratch/h9-host/src/main.jsx
import { createRoot } from 'react-dom/client'
import { Suspense, lazy } from 'react'

// C-8 에서 쓴 React.lazy 와 같은 문법인데, 가리키는 곳이 우리 파일이 아니다.
// 'like_app/LikeButton' 은 디스크에 없는 이름이다 — 실행 중에 네트워크로 찾아온다.
const LikeButton = lazy(() => import('like_app/LikeButton'))

createRoot(document.getElementById('root')).render(
  <main>
    <h1>피드</h1>
    <Suspense fallback={<p>불러오는 중…</p>}>
      <LikeButton initialCount={1240} />
    </Suspense>
  </main>,
)

lazySuspense는 C-8에서 회원가입 화면을 쪼갤 때 이미 썼는데, 문법이 똑같고 다른 것은 import() 안에 적힌 이름뿐입니다.

'like_app/LikeButton'이라는 파일은 이 저장소 어디에도 없고 빌드할 때도 없는데, 브라우저가 화면을 그리다가 저 줄에 도착해서야 5301로 받으러 갑니다.

진짜로 그려지는지 확인

두 앱을 각각 빌드하고 원격은 5301, 호스트는 5302로 띄운 뒤 호스트를 열어봤습니다.

텍스트
 <button aria-pressed="false">♡ 좋아요 1,240개</button>

떴는데, 이 버튼의 코드는 호스트 저장소에 한 줄도 없고 호스트가 아는 것은 주소 하나뿐이에요.

🙋 학생 질문 — "그럼 이건 그냥 iframe이랑 뭐가 다른가요?"

좋은 비교입니다. 예전에는 실제로 iframe으로 이런 걸 했어요.

가장 큰 차이는 같은 React 트리 안에 들어온다는 점인데, 방금 코드에서 initialCount={1240}을 props로 넘겼죠. iframe 이라면 그렇게 못 해서 메시지를 주고받는 별도 통로를 만들어야 하고, 스타일도 안 섞이고, 화면 크기 맞추는 것도 직접 해야 해요.

지금은 그냥 컴포넌트라서 props를 받고, 우리 CSS를 적용받고, 우리 Suspense 안에서 기다립니다.

그게 좋기만 한 건 아닌데, 같은 트리에 있다는 것은 저쪽이 터지면 이쪽도 영향을 받는다는 뜻이기도 합니다. Step 6에서 그 장면을 직접 보시게 됩니다.

💡 한 줄 정리

호스트는 원격의 코드를 갖지 않고 주소만 갖는다. 그 주소로 가는 시점은 빌드할 때가 아니라 화면을 그리는 중이다.


Step 3: 원격만 배포해도 호스트는 그대로

이게 이 구조를 쓰는 이유 전부입니다

여기까지는 "그래서 뭐가 좋은데?" 싶으실 거예요. 지금부터가 본론입니다.

좋아요 버튼 팀이 하트 모양을 바꾸기로 했다고 해봅시다. 원격 앱의 파일 하나만 고치고 원격만 다시 빌드해요. 호스트는 건드리지 않아서, 다시 빌드하지도 다시 띄우지도 않습니다.

확인할 수 있게 호스트 산출물의 이름을 먼저 적어뒀습니다.

호스트 번들 파일 화면
고치기 전 index-Jyffi1Md.js ♡ 좋아요 1,240개
원격만 재빌드 후 index-Jyffi1Md.js 🤍 좋아요 1,240개

호스트 번들은 파일 이름까지 똑같은데, Vite가 붙이는 해시는 내용이 바뀌면 따라 바뀌죠. 안 바뀌었다는 것은 내용이 한 바이트도 안 달라졌다는 뜻인데, 사용자가 보는 화면은 바뀌었습니다.

이걸 실무 언어로 옮기면, 좋아요 팀은 피드 팀의 배포 일정을 기다리지 않아도 된다는 뜻이에요. 피드 팀에 "저희 배포해야 하는데 언제 나가세요?" 하고 물을 일이 없는 거죠.

앱이 하나일 때는 이게 문제가 아닌데, 팀이 여섯이고 각자 주에 몇 번씩 배포하려 하면 그때부터 문제가 됩니다.

무엇을 산 것인지 정확히

한 가지 정확히 해둘 게 있는데, 우리가 산 것은 배포 독립성이지 개발 독립성이 아니라는 점이에요.

좋아요 팀이 props 이름을 initialCount에서 count로 바꾸면 호스트는 그대로 깨지는데, 그 사실은 배포하는 순간에야 알게 됩니다. 예전 같으면 빌드할 때 알았을 텐데요.

이 맞바꿈이 오늘의 나머지 전부입니다.

🙋 학생 질문 — "그럼 원격 팀이 잘못 배포하면 바로 사용자한테 나가는 거네요?"

맞는데, 그게 이 구조에서 가장 많이 하는 걱정이에요.

앱이 하나였을 때는 잘못된 코드가 사용자에게 가려면 빌드를 통과하고, 타입 검사를 통과하고, 테스트를 통과하고, 배포까지 가야 했는데, 관문이 여러 개였던 거죠.

원격은 그 관문들을 자기 저장소 안에서만 통과하고 호스트의 검사는 안 거치는데, 호스트 입장에서는 어느 날 갑자기 남의 코드가 자기 화면 안에서 돌기 시작하는 셈이에요.

이런 구조를 쓰는 조직은 그래서 보통 계약을 문서로 못 박고, 원격이 바뀌면 호스트 쪽 테스트를 자동으로 돌리는 장치를 따로 만드는데, 이게 나눈 대가로 새로 만들어야 하는 것들이죠. Step 5에서 이 비용을 다시 이야기합니다.

💡 한 줄 정리

원격만 다시 배포해도 호스트 산출물은 한 바이트도 안 바뀐다. 이 구조가 파는 것은 이것 하나이고, 나머지는 전부 그 대가다.


Step 4: React를 두 번 싣지 않는 일

미뤄둔 한 줄

Step 1부터 설정에 계속 있었지만 설명을 미뤄둔 줄이 있죠.

JavaScript
// scratch/h9-remote/vite.config.js — 지금까지 미뤄둔 부분
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },

호스트도 React를 쓰고 원격도 React를 쓰는데, 아무 조치도 안 하면 브라우저가 React를 두 벌 받게 돼요. 용량도 문제지만 더 큰 문제가 있는데, React는 훅 상태를 자기 내부에 들고 있어서 두 벌이 돌면 원격의 useState가 엉뚱한 React를 붙잡고 터집니다.

singleton: true는 "이 페이지 안에 React는 하나여야 한다"는 선언인데, 먼저 자리를 잡은 쪽 것을 다 같이 씁니다.

이름이 오해를 부릅니다

shared라는 이름 때문에 이게 최적화 옵션처럼 보이는데, 중복을 없애서 용량을 줄이는 것 같은 느낌이죠.

빼봤습니다.

먼저 빌드 결과부터 봅시다.

원격 산출물 청크 수
shared 있음 108,749 B 11개
shared 없음 84,520 B 7개

뺐더니 작아졌는데, 24 kB 나요. 빌드는 원격도 호스트도 exit 0으로 통과하고 경고도 없습니다.

여기서 멈추면 "아, shared는 오히려 용량을 늘리는 거였네"라고 결론 내리게 됩니다.

그런데 화면이 없습니다

작아진 원격을 띄우고 호스트를 열어봤습니다.

텍스트
 (빈 화면)

버튼이 안 나오는 정도가 아니라 <h1>피드</h1>까지 통째로 안 나오는데, 이건 호스트가 직접 쓴 글자예요.

이유는 이렇습니다. shared를 빼면 원격은 React를 어디서 얻을지 모르는 상태가 되고, 그래서 lazy가 실패하면서 그 실패가 위로 던져져 React 트리 전체가 죽습니다.

shared를 되돌리니 즉시 정상으로 돌아왔습니다.

⚠️ 두 가지를 함께 기억해두세요.

하나, shared는 최적화가 아니라 원격이 React를 얻는 유일한 통로입니다. 빼면 느려지는 게 아니라 안 돌아가요.

둘, 작아진 것이 좋아진 것이 아닙니다. 84,520 B는 108,749 B보다 작지만 그 작은 쪽은 아무것도 못 그립니다. 번들 크기만 보고 판단하면 정확히 반대로 읽게 돼요. 지난 세 시간 동안 번들을 들여다보며 계속 이야기한 것이 바로 이겁니다. 숫자는 그 자체로 좋고 나쁨을 말해주지 않아요.

🙋 학생 질문 — "그럼 shared를 쓰면 React가 정말 한 벌만 실리나요?"

브라우저가 받는 것은 한 벌이 맞습니다. 나중에 브라우저가 실제로 내려받은 파일 목록을 세어보는데, 원격에서 받아오는 파일 중에 React는 없고 호스트 것을 씁니다.

그렇긴 해도 원격의 dist 안에는 React 사본이 들어 있는데, 원격이 혼자서도 떠야 하기 때문이에요. 좋아요 팀이 자기 앱만 열어서 확인할 때는 React를 줄 호스트가 없으니까요.

정리하면 이렇게 갈립니다.

텍스트
 배포되는 것 : 호스트에도 React, 원격에도 React (예비용)
 전송되는 것 : 호스트 React 한 벌만

CDN에는 두 벌이 놓이고 사용자는 한 벌만 받는데, 용량을 이야기할 때 어느 쪽 숫자인지 밝히지 않으면 대화가 어긋나는 곳이에요.

💡 한 줄 정리

shared는 중복을 줄이는 옵션이 아니라 원격이 라이브러리를 얻는 통로다. 빼면 산출물은 작아지고 앱은 안 돈다.


Step 5: 그 대가를 잰다

같은 화면을 두 방식으로

값을 한번 매겨봅시다. 공정하게 재려면 완전히 같은 화면을 두 방식으로 만들어야 해요.

대조군도 하나 더 만들었는데, 같은 좋아요 버튼, 같은 <h1>피드</h1>, 같은 React 버전인데 페더레이션만 없습니다. 옆 파일에서 그냥 import 해요.

jsx
// scratch/h9-plain/src/main.jsx
import { createRoot } from 'react-dom/client'
import LikeButton from './LikeButton.jsx'

createRoot(document.getElementById('root')).render(
  <main>
    <h1>피드</h1>
    <LikeButton initialCount={1240} />
  </main>,
)

브라우저가 실제로 받는 것

용량은 두 가지로 이야기할 수 있는데, 배포되는 바이트와 사용자가 받는 바이트이고 Step 4에서 봤듯이 이 둘이 다릅니다.

사용자에게 중요한 것은 받는 쪽이라 그걸 쟀는데, 브라우저가 내려받기를 끝낸 파일들의 실제 전송량을 전부 더한 값이에요.

JS 파일 수 실제 전송량
페더레이션 없음 1개 59,882 B
페더레이션 24개 139,426 B

24개 중 10개는 원격에서, 14개는 호스트에서 오는데, 그 파일들 이름에는 loadShare__react__, loadRemote__like_app_ 같은 협상하는 코드들이 섞여 있어요.

버튼 하나 그리는 데 파일 24개입니다.

배포되는 바이트로 재면 190,664 B 對 428,411 B로 2.25배이고, 전송으로 재면 2.33배예요. 어느 쪽으로 재도 두 배가 넘습니다.

이 숫자를 어떻게 읽어야 할까요

여기서 조심할 게 있는데, 이 실습 앱은 버튼 하나짜리이고 진짜 앱은 훨씬 큽니다.

양쪽에 똑같은 코드를 조금씩 더해가면서 다시 재봤는데, 대조군과 페더레이션 쪽에 완전히 같은 양을 넣었어요.

더한 코드 대조군 페더레이션 배수 차이(세금)
없음 190,664 B 428,411 B 2.25배 237,747 B
조금 203,073 B 440,675 B 2.17배 237,602 B
좀 더 232,433 B 470,075 B 2.02배 237,642 B
많이 322,433 B 560,075 B 1.74배 237,642 B
아주 많이 556,453 B 794,073 B 1.43배 237,620 B

맨 오른쪽 칸을 보시면, 세금이 안 움직입니다. 앱을 세 배 가까이 키우는 동안 237,6xx B에서 오르내리는데, 가장 큰 값과 가장 작은 값의 차이가 145 B밖에 안 되는데도 배수는 2.25에서 1.43까지 내려갑니다.

텍스트
 세금(절대값)  237,747  237,620      거의 안 움직인다
 배수          2.25배     1.43배      계속 내려간다
 달라진 것     앱 크기 = 나누는 값

페더레이션이 물리는 비용은 앱 크기와 무관한 고정비인데, 협상 코드와 런타임은 앱이 크든 작든 똑같이 들어가서 앱이 작으면 세금이 앱보다 커 보이고 앱이 크면 묻힙니다.

⚠️ 그래서 "몇 배"라는 말은 그 자체로는 아무 정보가 없습니다. 무엇으로 나눴는지를 함께 말해야 해요.

커리큘럼에는 "소규모는 단일 앱이 더 빠르다"고 적혀 있는데, 이제는 그게 왜인지 아시겠죠. 의견이 아니라 나눗셈이라서, 고정비를 작은 수로 나누면 크고 큰 수로 나누면 작아요.

이 비용은 바이트로만 오지 않는데, 파일 1개가 24개가 됐죠. 요청이 늘고, 받아올 출처가 둘로 늘고, 그중 하나는 우리가 관리하지 않는 서버인데, H-3와 H-4에서 잰 지표들이 전부 여기에 영향을 받아요.

🙋 학생 질문 — "그럼 팀이 몇 개쯤 되면 쓸 만한 건가요?"

숫자로 딱 잘라 말하기는 어렵지만, 판단 기준은 드릴 수 있습니다.

이 구조가 파는 것은 Step 3에서 본 배포 독립성 하나예요. 그러니 질문은 "우리가 지금 배포 때문에 실제로 아파본 적이 있는가"입니다.

  • 다른 팀 배포를 기다리느라 우리 기능이 며칠씩 묶여 있었나요?
  • 남의 미완성 코드가 우리 배포에 섞여 들어와 사고가 난 적 있나요?
  • 앱 전체 빌드가 너무 느려서 배포 자체를 꺼리게 됐나요?

셋 다 아니라면 아직 아닙니다. 아프지 않은데 약을 먹는 셈이에요.

반대로 셋 중 여러 개가 계속 반복된다면, 그때는 오늘 잰 세금이 지불할 만한 값이 됩니다. 아픔이 실재할 때만요.

💡 한 줄 정리

세금은 앱 크기와 거의 무관한 고정비라서, "몇 배"는 분모를 밝히지 않으면 아무 말도 안 한 것과 같다.


Step 6: 경계를 넘는 약속은 조용히 깨진다

빌드 로그에 계속 있던 것

지금까지 빌드할 때마다 로그에 이런 게 찍히고 있었습니다.

텍스트
 Unable to compile federated types, Error: Command failed: ... tsc --showConfig
 error TS5058: The specified path does not exist: .../tsconfig.json

원격이 내보내는 것의 타입을 공유하는 단계가 실패하고 있었는데, 호스트가 LikeButton이 어떤 props를 받는지 타입으로 알 방법이 사라진 거예요.

그래도 vite buildexit 0이고, remoteEntry.js도 정상으로 나오고 화면도 잘 뜹니다.

우리는 이 모양을 여러 번 봤는데, 보안 헤더를 켰더니 빌드는 통과하는데 스크립트가 하나도 안 실렸던 것, 언어를 주소로 옮겼더니 빌드는 통과하는데 앱이 통째로 죽었던 것, 번역 칸을 빠뜨렸는데 빌드가 조용했던 것까지 오늘이 네 번째입니다.

이번 것이 특히 고약한 이유는 없어진 게 남의 팀과의 약속이라서인데, 앱이 하나일 때는 props 이름을 바꾸면 타입 검사가 즉시 잡아주지만 경계를 넘으면 그 안전망이 조용히 사라져요.

원격이 안 뜨면 어떻게 될까요

하나 더 봅시다. 원격 서버를 그냥 꺼버리고 호스트를 열었습니다.

텍스트
 (빈 화면)

Step 4에서 본 것과 똑같이 <h1>피드</h1>까지 안 나와요.

이건 생각해볼 만한 그림인데, 좋아요 버튼 하나를 못 가져왔을 뿐인데 피드 전체가 사라져서 남의 팀 서버가 우리 화면의 생사를 쥐고 있는 상태가 됩니다.

우리는 이미 막는 법을 압니다

다행히 이건 우리가 배운 걸로 막을 수 있는데, H-1에서 오류 경계를 앱에 이었고 H-2에서 그 경계가 무엇을 잡고 무엇을 못 잡는지까지 갈라봤죠.

원격을 불러오는 곳을 경계로 감싸면, 원격이 죽어도 그 칸만 비고 나머지 화면은 삽니다. 오류 보고도 그리로 올라가고요.

여기서 중요한 것은 기술이 아니라 순서인데, 앱이 하나일 때 오류 경계는 있으면 좋은 것이었어요. 화면을 남의 서버에서 가져오기 시작하는 순간 그것은 없으면 안 되는 것이 되는 셈인데, 같은 도구인데 지위가 달라졌죠.

이 아키텍처를 고르면 이런 것들이 줄줄이 따라오는데, 계약을 검사하는 장치, 원격이 죽었을 때의 대비, 버전이 어긋났을 때의 처리까지 전부 앱이 하나였으면 공짜로 얻던 것들이에요.

🙋 학생 질문 — "타입 공유가 실패한 건 설정을 잘못해서 아닌가요? 고치면 되잖아요."

맞습니다. 이 실습에서는 원격 폴더에 tsconfig.json을 안 만들어서 그런 거고, 만들면 타입 파일이 생성되는데, 짚고 싶은 건 고칠 수 있느냐가 아니라 안 고쳤을 때 아무도 안 알려준다는 점이에요.

빌드가 실패했다면 바로 고쳤겠죠. exit 0이라서 넘어갑니다. 로그를 위로 스크롤해서 읽어야만 보이고, 배포 파이프라인에서는 그 로그를 아무도 안 봐요.

타입이 없어도 화면은 멀쩡히 도는데, 문제가 드러나는 시점은 몇 주 뒤 원격 팀이 props 이름을 바꿨을 때이고, 그때는 원인을 찾기가 훨씬 어렵습니다.

💡 한 줄 정리

앱을 나누면 타입 검사와 빌드가 주던 안전망이 경계에서 끊긴다. 그 안전망을 대신할 것을 직접 만들어야 한다.


Step 7: 그래서 우리 앱엔 안 넣습니다

쓸지 말지 이전에

지금까지 재본 걸로 우리 앱에 넣을지 판단해봅시다.

판단하기 전에 확인할 게 있었는데요, 우리 풀스택 앱은 Next.js 16이라서 여기에 페더레이션을 붙이려고 하면 이렇게 됩니다.

텍스트
 npm error code ERESOLVE
 npm error Found: next@16.3.1
 npm error Could not resolve dependency:
 npm error peerOptional next@"^12 || ^13 || ^14 || ^15" from @module-federation/nextjs-mf@8.8.73

설치가 안 되는데, Next 전용 플러그인이 아직 15까지만 받고, 게다가 그 플러그인은 webpack을 전제로 하는데 Next 16은 다른 번들러가 기본이기 때문이에요.

쓸지 말지를 고민하기 전에 쓸 수 있는지에서 막힌 겁니다.

이런 일은 프론트엔드에서 드물지 않은데, 프레임워크가 메이저를 올리면 주변 도구들이 따라오는 데 시간이 걸리기 때문이에요. 새 아키텍처를 검토할 때 우리 스택에서 실제로 설치가 되는지부터 확인하는 게 순서인 이유가 여기 있는데, 블로그 글은 대개 그 이야기를 안 합니다.

판단표

우리 앱 기준으로 정리하면 이렇습니다.

물어볼 것 우리 앱 판정
팀이 여럿이고 배포 주기가 다른가 아니오
배포 때문에 실제로 아파봤나 아니오
앱이 세금을 묻을 만큼 큰가 아니오
우리 스택에서 설치는 되나 안 됨

넷 다 아니라서 안 넣습니다.

오늘 우리가 지은 원격과 호스트는 실습 폴더에 그대로 두고, 인스타그램 앱 본체는 한 줄도 바꾸지 않았습니다. 짓고, 재고, 안 넣기로 한 거예요.

이게 오늘 가장 중요한 부분인데, 배운 걸 안 쓰기로 결정하는 것도 결정이고, 재보고 내린 결정과 안 재보고 내린 결정은 다릅니다. 우리는 재봤어요.

이 분야가 지금 어디쯤 있나

마지막으로 동향을 짧게 짚어보려는데, 이 이야기를 하는 방식 자체가 오늘의 마지막 교훈이에요.

이 주제를 검색하면 "Module Federation 3.0"을 다루는 글이 여럿 나오는데, 2026년 마이크로 프론트엔드는 3.0 위에 서 있다는 식으로요.

확인해봤습니다.

확인한 곳 결과
패키지 저장소의 배포 이력 1,221개 전부 2.x · 최신 2.8.2
3.x로 나온 것 0개
공식 문서의 3.0 언급 0건
공식 로드맵 디버깅 도구와 서버 렌더링 고도화 (3.0 없음)

3.0은 존재하지 않습니다. 존재하지 않는 버전에 대한 사용법과 마이그레이션 안내가 검색 결과 상단에 있는 거예요.

반면 함께 언급되는 Native Federation 쪽은 실재하는데, 브라우저 표준인 import maps를 써서 번들러 없이 모듈을 나눠 쓰자는 접근이고, 도구도 활발히 관리되고 있어요. 아직 널리 쓰이는 방식은 아니지만, 오늘 우리가 본 문제들(공유 의존성, 계약, 원격 장애)은 그쪽에서도 그대로 남아서 짐을 옮기는 것이지 없애는 게 아니에요.

⚠️ 그러니 버전 이야기를 만나면 검색 결과가 아니라 패키지 저장소를 보세요. 저장소는 실제로 배포된 것만 들고 있습니다. 블로그는 아무거나 쓸 수 있는데, 이 습관 하나가 여러분을 없는 기능을 찾아 헤매는 하루에서 구해줍니다.

🙋 학생 질문 — "그럼 오늘 배운 건 결국 안 쓰는 건데 왜 배운 건가요?"

세 가지 이유가 있습니다.

첫째, 여러분이 취업할 회사 중 일부는 이미 이걸 쓰고 있는데, 그 코드베이스에 들어갔을 때 remoteEntry.jsshared가 무엇인지 알고 시작하는 것과 모르고 시작하는 것은 다릅니다.

둘째, 안 쓰기로 결정하려면 알아야 합니다. 누군가 회의에서 "우리도 마이크로 프론트엔드로 가야 하지 않나요"라고 할 때, 오늘 잰 숫자들을 알고 있으면 근거를 갖고 대화할 수 있는데, 모르면 분위기로 결정하게 됩니다.

셋째, 오늘 나온 것들은 이 기술에만 해당하는 게 아닌데, 고정비를 무엇으로 나눌 것인가, 경계를 만들면 안전망이 어디서 끊어지는가, 빌드가 초록이라는 게 무엇을 보장하고 무엇을 안 하는가 하는 것들이 다른 판단에도 그대로 쓰입니다.

💡 한 줄 정리

새 아키텍처는 쓸지 말지 이전에 우리 스택에서 되는지부터 확인하고, 버전은 검색 결과가 아니라 패키지 저장소에 물어본다.


마무리

오늘 한 일을 정리하면 이렇습니다.

텍스트
 1. 원격 앱이 내보낼 것을 골랐다        exposes 하나 · remoteEntry.js 17.13 kB
 2. 호스트가 실행 중에 가져왔다         디스크에 없는 이름을 import 한다
 3. 원격만 배포해도 호스트가 그대로     번들 해시 index-Jyffi1Md.js 불변
 4. shared 를 빼봤다                    산출물은 작아지고 화면은 전멸
 5. 세금을 쟀다                          JS 1개 59,882 B  24개 139,426 B
 6. 경계에서 안전망이 끊기는 걸 봤다     타입 공유 실패인데 빌드는 exit 0
 7. 안 넣기로 했다                       그리고 3.0 은 존재하지 않았다

오늘은 이 과목에서 처음으로 결론이 "안 한다"인 시간이었는데, 안 하기로 하는 데 한 시간을 다 썼죠. 재보지 않고 안 하는 것과 재보고 안 하는 것은 완전히 다른 결정이니까요.

60개 모듈을 돌아봅니다

이걸로 이 과목이 끝납니다.

처음에 여러분은 순수 HTML, CSS, JavaScript만 아는 상태로 오셨는데, 그때 만든 인스타그램 클론은 document.createElement로 카드를 그리는 정적인 화면이었습니다.

지금 그 프로젝트는 이렇습니다. 타입이 붙어 있고, 컴포넌트로 나뉘어 있고, 주소마다 화면이 갈리고, 서버 상태를 캐시하고, 실시간으로 메시지가 오고, 서버에서 미리 그려져서 오고, 로그인이 진짜로 되고, 남의 스크립트를 막고, 테스트가 천 건 넘게 지키고 있고, 오류와 속도가 측정되고, 네 나라 말을 합니다.

정말 남기고 싶은 것은 그 목록이 아니에요.

이 과목 내내 반복된 게 하나 있습니다. 재보기 전에는 모른다는 것요. 통설이 틀린 적이 여러 번 있었고, 제가 쓴 설명이 실행해보니 거짓이었던 적도 여러 번 있었는데, 빌드가 통과했는데 앱이 죽어 있던 적은 오늘까지 네 번이고, 커리큘럼에 적힌 내용이 실제와 달랐던 적도 있었죠. 오늘은 아예 존재하지 않는 버전이 검색 결과 상단에 있었습니다.

그때마다 답을 준 것은 실행이었습니다. 재보고, 조건을 바꿔서 다시 재보고, 안 바뀌면 재는 방법을 의심하고요.

프레임워크는 계속 바뀌고 오늘 배운 버전들도 몇 달 뒤면 낡는데, 어떻게 확인할지 아는 것은 안 낡습니다. 여러분이 가져가실 게 그거였으면 좋겠습니다.

수고하셨습니다.


과제

[구현] 원격이 죽어도 화면이 살아남게 만드세요

Step 6에서 원격 서버를 끄면 호스트 화면이 통째로 사라지는 걸 봤습니다.

  • LikeButton을 불러오는 곳을 오류 경계로 감싸고, 원격을 끈 상태로 다시 열어보세요.
  • <h1>피드</h1>과 나머지 화면이 남아 있나요? 좋아요 칸에는 무엇이 보이나요?
  • 경계가 없을 때와 있을 때, 사용자가 겪는 일이 어떻게 다른지 한 문단으로 적어보세요.

[구현] 원격이 약속을 어기게 만들어보세요

좋아요 팀이 props 이름을 바꿨다고 해봅시다.

  • 원격의 LikeButtoninitialCount 대신 count를 받도록 고치고 원격만 다시 빌드하세요.
  • 호스트는 건드리지 마세요. 화면에 무엇이 나오나요?
  • 호스트에서 타입 검사와 빌드를 돌려보세요. 이 변경을 잡아주나요?
  • 앱이 하나였다면 이 실수가 언제 잡혔을지 비교해서 적어보세요.

[탐구] 그 세금을 우리 인스타 앱에 매겨보세요

Step 5의 표는 실습용 앱으로 잰 것입니다. 우리가 진짜로 만든 앱에 매기면 얼마일까요.

  • 우리 인스타 클론을 빌드해서 지금 번들이 몇 바이트인지 확인하세요. 번들을 들여다보는 방법은 지난 시간들에 다뤘습니다.
  • 그 크기를 분모로 놓고, Step 5에서 잰 고정비를 나눠보세요. 몇 퍼센트인가요?
  • 우리 앱을 몇 조각으로 나눈다고 하면 그 세금은 조각 수만큼 늘어날까요, 아니면 한 번만 낼까요? 왜 그렇게 생각하는지 근거를 적어보세요.
  • 우리는 번들에 상한을 걸어둔 적이 있습니다. 이 세금을 더하면 그 상한을 넘나요?

[탐구] 검색 결과를 저장소로 검증해보세요

Step 7에서 존재하지 않는 버전이 검색 상단에 있는 걸 봤습니다.

  • 여러분이 요즘 관심 있는 라이브러리를 하나 골라 "새 메이저 버전"을 검색해보세요.
  • 패키지 저장소에서 실제 배포 이력과 최신 버전도 확인하세요.
  • 둘이 일치하나요? 어긋난다면 어긋난 글이 무엇을 근거로 그렇게 썼는지 살펴보세요.
  • 앞으로 버전을 확인할 때 여러분이 쓸 순서를 세 줄로 적어보세요.

생각해볼 주제

1. 조직도가 아키텍처를 정할 때

오늘 배운 구조는 기술 문제를 푸는 도구처럼 보이지만, 실제로 푸는 것은 사람 문제에 가깝습니다. 팀이 서로 기다리지 않게 하는 것이니까요.

그런데 반대 방향도 자주 일어납니다. 팀을 여섯 개로 나눴으니 화면도 여섯 조각으로 나눠야 한다고 생각하는 경우요.

조직 구조에 맞춰 아키텍처를 정하는 것은 어디까지 합리적이고 어디부터 문제일까요. 팀이 합쳐지거나 갈라질 때 화면 구조도 따라가야 할까요?

2. 안 아픈데 약을 먹게 되는 이유

오늘 우리는 재보고 안 쓰기로 했습니다. 그런데 실무에서는 재보지 않고 도입하는 일이 훨씬 흔해요.

큰 회사가 쓴다는 이유로, 컨퍼런스에서 봤다는 이유로, 이력서에 쓰고 싶다는 이유로 도입되는 기술이 있습니다. 그리고 도입된 뒤에는 되돌리기가 훨씬 어렵죠.

여러분이 팀에서 "이거 도입합시다"라는 제안을 받았을 때, 무엇을 물어보면 이 판단을 도울 수 있을까요. 그리고 반대로 여러분이 제안하는 쪽이라면 무엇을 미리 준비해 가시겠어요?

3. 없는 것에 대한 설명서가 존재할 때

오늘 확인한 것 중 가장 이상한 건 존재하지 않는 버전의 사용법이 검색 결과 상단에 여러 개 있었다는 겁니다.

그럴듯한 글은 이제 아주 싸게 만들어집니다. 그리고 검색은 그럴듯함과 사실을 잘 구별하지 못해요.

이런 환경에서 여러분은 무엇을 1차 자료로 삼으시겠어요? 그리고 팀 안에서 누군가 없는 기능을 근거로 설계를 제안했을 때, 그 사람을 무안하게 하지 않으면서 사실을 확인하려면 어떻게 말하는 게 좋을까요?

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안입니다. 직접 해보신 뒤에 펼쳐서 맞춰보세요.


🎯 [과제 1 예시답안] 원격이 죽어도 화면이 살아남게 만드세요

채점 포인트

항목 배점 확인 내용
경계를 원격 불러오는 곳에 감쌌는가 7 앱 전체가 아니라 그 칸만 감싸야 뜻이 있다
원격을 끈 상태로 실제로 확인 7 켠 채로 확인하면 아무것도 검증 못 한다
남은 화면과 대체 문구를 정확히 서술 6 <h1>피드</h1> 생존 · 좋아요 칸만 대체
경계 유무의 차이를 사용자 관점으로 서술 5 빈 화면 vs 한 칸만 빈 화면
합계 25

풀이 예시

원격을 불러오는 곳만 감싸는 것이 핵심인데, 앱 전체가 아니라 그 칸만 경계로 감싸야 뜻이 있습니다.

jsx
// scratch/h9-host/src/RemoteBoundary.jsx
import { Component } from 'react'

export default class RemoteBoundary extends Component {
  state = { failed: false }
  static getDerivedStateFromError() { return { failed: true } }
  render() {
    if (this.state.failed) return <p>좋아요를 지금 불러올 수 없어요.</p>
    return this.props.children
  }
}

이어서 원격을 쓰는 칸만 감싸면 됩니다.

jsx
// scratch/h9-host/src/main.jsx
<main>
  <h1>피드</h1>
  <RemoteBoundary>
    <Suspense fallback={<p>불러오는 중…</p>}>
      <LikeButton initialCount={1240} />
    </Suspense>
  </RemoteBoundary>
</main>

원격 서버를 끄고 열어본 결과는 이렇습니다.

경계 없음 경계 있음
<h1>피드</h1> 안 나옴 나옴
좋아요 칸 안 나옴 좋아요를 지금 불러올 수 없어요.
사용자가 보는 것 빈 화면 한 칸만 빈 화면

자주 나오는 실수

앱 전체를 감싸는 것. createRoot 바로 안쪽을 감싸면 원격이 죽었을 때 여전히 화면 전체가 대체 문구로 바뀌는데, 경계는 잃어도 되는 범위에 맞춰 그어야 뜻이 있어요.

원격을 켠 채로 확인하는 것. 화면이 잘 나오는 걸 보고 통과로 적는 경우가 있는데, 그건 경계가 일하는지 안 하는지를 전혀 안 잰 것이라서 일부러 꺼봐야 확인이 됩니다.

💡 튜터의 한마디

H-1에서 오류 경계를 앱에 이을 때는 "있으면 좋은 것"이었습니다. 그런데 화면 일부를 남의 서버에서 가져오기 시작하는 순간 그건 없으면 안 되는 것이 돼요. 도구는 그대로인데 지위가 바뀌는 셈이라, 아키텍처를 고르면 이런 것들이 따라옵니다.


🎯 [과제 2 예시답안] 원격이 약속을 어기게 만들어보세요

채점 포인트

항목 배점 확인 내용
원격만 고치고 원격만 재빌드했는가 5 호스트를 건드리면 실험이 성립 안 한다
화면에 나온 결과를 정확히 적었는가 7 NaN개 — 안 죽고 틀린 값이 보인다
호스트 빌드·타입 검사 결과를 확인 7 둘 다 통과한다(exit 0)
앱이 하나였을 때와 비교 6 그때는 타입 검사가 즉시 잡는다
합계 25

풀이 예시

원격에서 props 이름만 바꿉니다.

jsx
// scratch/h9-remote/src/LikeButton.jsx — 좋아요 팀이 이름을 바꿨다
export default function LikeButton({ count }) {
  const [liked, setLiked] = useState(false)
  const total = count + (liked ? 1 : 0)
  // ... total.toLocaleString('ko-KR')
}

원격만 다시 빌드하고, 호스트는 그대로 둔 채 화면을 열면 이렇게 나옵니다.

텍스트
 <h1>피드</h1>
 <button aria-pressed="false">♡ 좋아요 NaN개</button>

호스트는 여전히 initialCount={1240}을 넘기는데 원격은 count를 읽으니 undefined를 받고, 거기에 0을 더해 NaN이 됩니다.

이어서 호스트에서 검사를 돌려보면 이렇습니다.

검사 결과
호스트 빌드 exit 0
타입 검사 못 잡는다
과제 1 의 오류 경계 안 걸린다
사용자 화면 좋아요 NaN개

이게 왜 가장 나쁜 실패인가

과제 1의 경계가 안 걸리는 이유가 중요한데, 예외가 안 났기 때문이에요. undefined + 0은 터지지 않고 조용히 NaN이 됩니다.

정리하면 이 실패는 이렇습니다.

텍스트
 빌드가 알려주나?      아니오
 타입이 알려주나?      아니오
 오류 경계가 잡나?     아니오
 오류 추적에 올라가나?  아니오
 사용자가 보나?        예

모든 안전망을 통과해서 사용자에게만 보입니다.

앱이 하나였다면 props 이름을 바꾸는 순간 타입 검사가 빨간 줄을 그었을 거예요. 그런데 경계를 넘으면 그 안전망이 사라집니다.

자주 나오는 실수

원격 빌드가 실패한 걸 모르고 화면을 읽는 것. 이름을 바꾸다 문법 오류가 나면 빌드가 실패하는데, 서버는 직전 빌드 결과를 계속 서빙하다 보니 화면이 멀쩡해 보이고 "안 깨지네?"라는 틀린 결론이 나와요. 빌드 로그에서 성공을 먼저 확인하고 화면을 보세요.

💡 튜터의 한마디

이 과목에서 "빌드는 통과하는데 뭔가 잘못돼 있다"를 여러 번 만났는데, 오늘 것이 그중 가장 조용합니다. 나눈다는 것은 안전망을 스스로 다시 만들어야 한다는 뜻인데, 계약을 검사하는 장치를 따로 두는 조직이 많은 이유가 이겁니다.


🎯 [과제 3 예시답안] 그 세금을 우리 인스타 앱에 매겨보세요

채점 포인트

항목 배점 확인 내용
실제 앱을 빌드해서 크기를 확인 6 짐작이 아니라 빌드 출력에서
고정비를 나눠 비율을 계산 6 약 35%(압축 전 기준)
조각 수만큼 늘어나는지 근거와 함께 판단 7 원격마다 자기 런타임을 싣는다
번들 상한을 넘는지 확인 6 넘는다
합계 25

풀이 예시

먼저 우리 앱을 빌드합니다.

텍스트
 dist/assets/index-BzFlim_B.js   683.01 kB │ gzip: 221.72 kB
 [예산] gzip 219,472 B / 예산 225,000 B

Step 5에서 잰 고정비는 압축 전 237,620 B였습니다. 나눠보면

텍스트
 237,620 ÷ 683,010 = 34.8%

우리 앱 크기의 3분의 1이 넘는데, Step 5 표에서 배수가 1.43까지 내려갔던 그 "아주 많이" 칸이 556,453 B 였으니 우리 앱(683,010 B)은 그보다 조금 큰 편이고, 배수로는 1.3배 대에 들어갑니다.

상한을 넘는가

우리는 번들에 상한을 걸어뒀는데, 압축 기준으로 다시 재면 세금은 71,409 B입니다.

지금 gzip 219,472 B
상한 225,000 B
남은 여유 5,528 B
세금(gzip) 71,409 B
더하면 290,881 B

상한을 65,881 B 초과합니다. 여유가 5,528 B인데 세금이 그 열세 배예요. 그래서 페더레이션을 도입하는 순간 빌드가 멈춥니다.

조각 수만큼 늘어나는가

늘어나는데, 근거는 브라우저가 받아간 파일 목록에 있어요.

Step 5에서 24개 파일을 셀 때, 같은 이름의 런타임 파일이 두 출처 양쪽에 있었습니다.

텍스트
 localhost:5302/assets/dist-_0qr8SzR.js    호스트에서
 localhost:5301/assets/dist-_0qr8SzR.js    원격에서

내용이 같은데 출처가 다르니 브라우저는 따로 받는데, 원격이 셋이면 같은 런타임을 넷이 받게 되고, 원격마다 라이브러리 예비 사본도 각자 들고 있습니다.

정리하면, 세금은 한 번 내고 끝이 아니라 조각마다 붙습니다.

💡 튜터의 한마디

번들 상한을 걸어둔 것이 여기서 값을 하는데, 그때는 "혹시 모르니 재게 해두자"였던 게 오늘 아키텍처를 검토하면서 판단 근거가 됐어요. 재는 장치를 미리 만들어두면 나중에 결정할 때 쓸 숫자가 이미 있습니다.


🎯 [과제 4 예시답안] 검색 결과를 저장소로 검증해보세요

채점 포인트

항목 배점 확인 내용
검색 주장과 저장소 사실을 나란히 제시 7 둘을 대조한 형태여야 한다
최신 버전과 배포 이력을 확인 6 태그만 보면 안 된다
어긋난 글의 근거를 살펴봄 5 대개 근거가 없거나 로드맵을 오독
앞으로 쓸 순서를 정리 7 재현 가능한 절차로
합계 25

풀이 예시

Step 7에서 다룬 것과 같은 방식을 React로도 해봤습니다. 검색하면 "React 20"을 다루는 글이 나오거든요.

텍스트
 latest       19.2.8
 canary       19.3.0-canary-...
 20.x 로 배포된 것: NONE

배포된 20.x가 하나도 없습니다. 다음 버전 후보조차 19.3이에요.

Module Federation도 같습니다.

검색이 말하는 것 저장소가 말하는 것
Module Federation "3.0 이 표준" 배포 1,221개 전부 2.x · 최신 2.8.2
React "React 20 새 기능" 최신 19.2.8 · 20.x 0개

어긋난 글은 무엇을 근거로 썼나

살펴보면 대개 셋 중 하나입니다.

  • 로드맵을 릴리스로 오독 — 논의 중인 계획을 나온 것처럼 씀
  • 다른 것과 혼동 — 곁가지 프로젝트나 실험 브랜치를 메이저 버전으로 부름
  • 근거 없음 — 그럴듯한 문장만 있고 출처가 아예 없음

세 번째가 가장 많고, 요즘은 이런 글을 만드는 비용이 거의 0이라 계속 늘어납니다.

앞으로 쓸 순서

텍스트
 1. 패키지 저장소에서 최신 버전과 배포 이력을 본다
    (태그만 보지 말고 실제 배포 목록까지 — 태그는 사람이 붙인다)
 2. 공식 문서에서 그 버전이 언급되는지 확인한다
 3. 우리 스택에 실제로 설치가 되는지 넣어본다

3번이 특히 중요한데, Step 7에서 봤듯 버전이 존재해도 우리 프레임워크가 안 받으면 못 씁니다.

자주 나오는 실수

latest 태그만 보고 끝내는 것. 태그는 배포하는 사람이 붙이는 이름이라 실수로 옮겨 붙기도 하고 미리 붙어 있기도 하는데, 실제 배포 목록을 함께 보면 그런 착오가 안 생겨요.

💡 튜터의 한마디

이 습관은 오늘 배운 것 중에 가장 오래 쓰실 텐데, 프레임워크는 계속 바뀌지만 "실제로 배포된 것만 저장소에 있다"는 성질은 안 바뀌거든요.


생각해볼 주제


🤔 [생각해볼 주제 1] 조직도가 아키텍처를 정할 때

문제 상황 요약

오늘 배운 구조가 실제로 푸는 것은 기술 문제라기보다 사람 문제에 가까운데, 팀이 서로 기다리지 않게 하는 것이니까요. 반대로 "팀이 여섯이니 화면도 여섯 조각"이라는 식으로 조직도를 그대로 화면에 옮기는 일도 흔합니다.

튜터의 가이드 및 해설

소프트웨어 구조가 그것을 만든 조직의 소통 구조를 닮는다는 관찰은 오래전부터 있었는데, 팀이 나뉘어 있으면 코드도 그 경계를 따라 나뉘는 경향이 있어요. 자연스러운 일이고 어느 정도는 좋은 일이기도 한데, 경계가 사람의 책임과 맞아떨어지면 누가 무엇을 고칠지가 분명해지니까요.

문제는 방향을 거꾸로 잡을 때 생기는데, 화면을 쪼개는 이유가 "사용자 경험상 이 조각들이 독립적이어서"가 아니라 "우리 팀이 그렇게 나뉘어 있어서"가 되면 사용자는 팀 경계를 화면에서 느끼게 됩니다. 조각마다 버튼 모양이 미묘하게 다르고, 로딩 방식이 다르고, 어떤 칸만 유난히 느려요.

판단할 때 이렇게 물어보시면 좋습니다. 이 경계를 사용자가 알아챌 수 있는가? 알아챌 수 있다면 그 경계는 화면의 경계가 아니라 조직의 경계일 가능성이 큽니다.

조직은 생각보다 자주 바뀌는데, 팀이 합쳐지고 갈라지죠. 그때마다 화면 구조를 따라 바꿀 수는 없어서, 조직보다 덜 바뀌는 것을 경계로 잡는 편이 안전합니다. 예를 들어 "메시지"라는 도메인은 팀 구성이 바뀌어도 남지만, "3팀이 맡은 부분"은 안 남습니다.

한 가지 더 짚으면, 오늘 잰 세금은 조각마다 붙었죠. 조직도를 그대로 옮기면 조각 수가 조직 크기를 따라가고 세금도 함께 따라가는데, 조직이 커질수록 사용자가 더 많이 내는 구조가 되는 거예요. 그건 아무도 의도한 적 없는 설계입니다.

🎯 면접관을 홀리는 핵심 멘트

"구조가 조직을 닮는 건 자연스러운 일이지만, 사용자가 그 경계를 알아챈다면 잘못 그은 겁니다. 저는 경계를 팀이 아니라 도메인에 긋습니다. 팀은 바뀌고 도메인은 남으니까요."


🤔 [생각해볼 주제 2] 안 아픈데 약을 먹게 되는 이유

문제 상황 요약

오늘 우리는 재보고 안 쓰기로 했는데, 실무에서는 재보지 않고 도입하는 일이 훨씬 흔하죠. 큰 회사가 쓴다는 이유로, 컨퍼런스에서 봤다는 이유로 도입하는 경우가 많은데, 도입한 뒤에는 되돌리기가 훨씬 어렵습니다.

튜터의 가이드 및 해설

이런 결정이 나쁜 의도에서 나오는 경우는 거의 없고, 대개는 좋은 것을 하고 싶어서 생기는데, 문제는 "좋은 것"의 근거가 우리 상황이 아니라 남의 상황이라는 데 있습니다.

큰 회사가 마이크로 프론트엔드를 쓰는 이유는 팀이 수십 개고 배포가 서로를 막고 있기 때문인데, 그 회사는 오늘 우리가 잰 세금을 내고도 남는 것을 얻어요. 같은 도구를 팀 하나짜리 회사가 도입하면 세금만 내고 얻는 게 없는데, 도구가 나쁜 게 아니라 분모가 다른 겁니다.

제안을 받았을 때 물어볼 것은 기술 질문이 아니라 아픔 질문입니다.

  • 이 문제로 최근에 실제로 곤란했던 적이 언제였나요?
  • 그때 손해가 얼마였나요? 시간이든 사고든 숫자로요.
  • 지금 방식으로 그걸 줄일 방법은 정말 없나요?
  • 도입하면 새로 생기는 일은 무엇인가요?

마지막 질문이 자주 빠집니다. 그런데 오늘 우리가 본 것만 해도 계약 검사, 원격 장애 대비, 버전 정합이 새로 생겼죠. 도입 비용은 대개 첫 설치가 아니라 그 뒤로 계속 드는 유지 비용입니다.

제안하는 쪽이라면, 저는 오늘처럼 작게 재보고 가는 걸 권하는데, 실습용으로 하나 지어서 숫자를 만들고 그 숫자를 들고 이야기하는 거예요. "좋대요"와 "우리 앱 기준으로 번들이 35% 늘고 상한을 넘습니다"는 완전히 다른 대화인데, 후자는 반대하는 사람도 납득시키고 통과되더라도 무엇을 감수하는지 모두가 알고 시작하게 돼요.

되돌리기 어렵다는 점도 미리 말해두는 게 좋은데, 나중에 "그때 그런 말 없었잖아요"가 안 나오게요.

🎯 면접관을 홀리는 핵심 멘트

"저는 도입 검토를 할 때 기술이 좋은지가 아니라 우리가 그 문제로 아파본 적이 있는지를 먼저 봅니다. 안 아픈데 약을 먹으면 부작용만 남으니까요. 그리고 그 판단은 인상이 아니라 우리 앱에서 잰 숫자로 합니다."


🤔 [생각해볼 주제 3] 없는 것에 대한 설명서가 존재할 때

문제 상황 요약

오늘 확인한 것 중 가장 이상한 일은 존재하지 않는 버전의 사용법이 검색 결과 상단에 여러 개 있었다는 건데, 그럴듯한 글은 이제 아주 싸게 만들어지고 검색은 그럴듯함과 사실을 잘 구별하지 못합니다.

튜터의 가이드 및 해설

먼저 실용적인 이야기부터 하죠. 1차 자료를 정해두는 게 좋습니다.

버전과 API에 대해서는 순서가 분명한데, 패키지 저장소가 가장 강한데 실제로 배포된 것만 있으니까요. 그다음이 공식 문서이고, 그다음이 저장소의 이슈나 논의입니다. 그런데 블로그와 튜토리얼은 이해를 돕는 용도로는 훌륭하지만 사실 확인용으로는 가장 약합니다.

가장 강한 자료가 하나 더 있는데, 설치해서 직접 돌려보는 겁니다. 오늘 우리가 한 게 그거였고, 그 덕에 "쓸 수 있는지"에서 막힌다는 걸 알았죠.

다음은 사람 이야기입니다. 팀에서 누군가 없는 기능을 근거로 설계를 제안했을 때요.

핵심은 틀렸다고 말하지 않고 함께 확인하자고 말하는 것입니다. 사람과 정보를 분리하는 거예요. "그건 없어요"는 상대를 틀린 사람으로 만들지만, "저도 그 얘기 봤는데 저장소에서 같이 확인해볼까요?"는 둘이 같은 편에서 정보를 검증하는 모양이 되는데, 결과가 어느 쪽이든 관계가 안 상해요.

이 방식에는 부수 효과도 있는데, 확인하는 절차가 팀의 습관이 됩니다. 한 번 같이 저장소를 열어보면 다음부터 그 사람도 혼자 열어보는데, 지적은 그 사람 한 명을 고치지만 절차는 팀을 고칩니다.

마지막으로, 이 문제는 앞으로 더 심해질 텐데, "내가 어떻게 확인했는지"를 함께 적어두는 습관을 권합니다. 결론만 적으면 나중에 그 결론이 또 하나의 출처 없는 주장이 되거든요. 확인한 방법이 같이 적혀 있으면 다음 사람이 다시 확인할 수 있습니다.

🎯 면접관을 홀리는 핵심 멘트

"정보가 싸게 만들어지는 시대일수록 1차 자료의 순서를 정해두는 게 중요하다고 봅니다. 버전 이야기는 저장소에 묻고, 되는지는 설치해서 확인합니다. 그리고 팀에는 결론이 아니라 확인한 방법을 남깁니다."

전체 목록 리액트
이 토픽을 끝까지 봤어요 🎉 다른 트랙도 둘러보기 →