Skip to content

Instantly share code, notes, and snippets.

@leedc0101
Created February 16, 2026 01:52
Show Gist options
  • Select an option

  • Save leedc0101/2bfed7778acadd7fba2ac5d4867119e5 to your computer and use it in GitHub Desktop.

Select an option

Save leedc0101/2bfed7778acadd7fba2ac5d4867119e5 to your computer and use it in GitHub Desktop.
frontend-briefing-2026-02-16-KST

How We’re Surviving 600+ Legacy Angular Components While Migrating to Next.js, GraphQL, and a Monorepo

소개

수명이 긴 프론트엔드에서 흔히 겪는 문제는 비슷합니다. 앱이 커지고 기능이 쌓이면서 기술 부채가 늘고, 개발 속도가 점점 느려집니다.

글의 팀도 그 상황이었습니다.

Angular 14 기반의 큰 애플리케이션(컴포넌트 600개+)이 있었고, 단일(모놀리식) 구조와 복잡성 증가로 개발이 계속 어려워졌습니다. 전면 재작성(빅뱅 리라이트)은 매력적이지만 비용·리스크·비즈니스 중단이 너무 큽니다.

그래서 “한 번에 갈아엎기” 대신, 아래 도구/패턴을 조합해 점진적 마이그레이션을 설계했습니다.

  • Next.js
  • GraphQL Federation
  • 모노레포 아키텍처
  • 프레임워크 사이를 잇는 브릿지로서 Web Components

이 글은 아키텍처 구성, 마이그레이션 접근, 그 과정의 교훈을 정리합니다.

레거시 환경: 시작점

기존 Angular 앱은 오랫동안 비즈니스의 핵심이었고, 아래 기능을 담당했습니다.

  • 고객/사용자 등록 플로우
  • 서비스 제공자 검색
  • 결제 처리
  • 역할 기반 사용자 관리
  • 복잡한 다단계 폼
  • AWS Cognito 인증

동작은 했지만, 시간이 지나면서 한계가 뚜렷해졌습니다.

주요 문제

  • 모놀리식 아키텍처

    • 루트 모듈 1개에 637개 컴포넌트 선언 → 코드 이해/변경이 어려움
  • 수동 의존성 주입

    • 커스텀 HTTP 서비스를 60곳 이상에서 직접 생성 → Angular DI를 우회
  • 강한 결합

    • 컴포넌트가 특정 API 응답 형태에 직접 묶임
  • 재사용성 부족

    • UI 컴포넌트가 Angular 전용 → 다른 곳에서 재사용 불가
  • 빌드 느림

    • 앱이 커지며 빌드 시간이 계속 증가
  • 기술 부채

    • Angular 14
    • Bootstrap 4
    • jQuery 의존

여러 환경(dev/test/uat/prod)까지 운영 중이라, 전면 재작성은 12~18개월 이상 걸리고 비즈니스 리스크가 큽니다.

전체 그림(구성 요소가 어떻게 맞물리는가)

높은 수준의 구조는 다음과 같습니다.

  • Angular는 레거시 UI를 계속 실행
  • 신규 기능은 React로 개발
  • React 앱을 Web Components 형태로 배포
  • GraphQL이 프론트엔드와 백엔드 사이의 API 레이어
  • Next.js가 인증을 담당

이렇게 하면 비즈니스 운영을 깨지 않고 기능 단위로 하나씩 교체할 수 있습니다.

마이그레이션 전략: Strangler Fig 패턴

기존 시스템을 유지한 채, 일부를 새로운 구현으로 점점 감싸서 교체하는 Strangler Fig 패턴을 채택했습니다.

핵심 기둥 3개:

  • 모노레포 기반
  • GraphQL 기반 API
  • Web Components 브릿지

pnpm + Turborepo 모노레포

pnpm + Turborepo로 모노레포를 구성했습니다.

예시 구조:

monorepo/
├── apps/
│   ├── auth-service/
│   ├── graphs/
│   ├── services/
│   └── notification-service/
├── packages/
│   ├── design-system/
│   ├── authentication/
│   ├── logger/
│   ├── database/
│   └── web-components/

기대/체감 효과

  • 앱 간 공용 코드 공유
  • 엔드투엔드 TypeScript
  • 빌드 속도 개선(70% 향상)
  • 스택 전반 변경을 하나의 PR로 처리(원자적 PR)
  • 버전 관리/릴리스 조율 쉬움

GraphQL을 API 레이어로

모놀리식 REST 대신, 도메인 기반 GraphQL 서비스들로 분리했습니다.

예시 스키마:

type Business {
  id: ID!
  name: String!
  subscriptionPlans: [Plan!]!
  defaultPlanId: Int
}

type Query {
  searchBusinesses(country: String!, searchTerm: String!): [Business!]!
}

장점

  • 도메인 경계가 명확해짐
  • 독립 배포 가능
  • 강한 타입
  • 클라이언트가 필요한 데이터만 요청
  • Federation 준비(확장) 구조

Web Components: Angular 안에 React를 심기

React 기능을 Angular 앱 안에 끼워 넣기 위해 Web Components를 사용했습니다.

React 컴포넌트를 Web Component로 래핑:

import { r2wc } from '@r2wc/react-to-web-component';
import { UserRegistrationWithApollo } from './UserRegistration';

const UserRegistrationWC = r2wc(UserRegistrationWithApollo, {
  props: {
    businessGraphApiUrl: 'string',
    providerGraphApiUrl: 'string',
  },
});

customElements.define('user-registration', UserRegistrationWC);

Angular에서 사용:

<user-registration
  [businessGraphApiUrl]="businessApiUrl"
  [providerGraphApiUrl]="providerApiUrl">
</user-registration>

왜 효과적이었나

  • 프레임워크에 덜 종속적인 UI 구성
  • 점진적 마이그레이션 가능
  • 현대적인 React 패턴 활용
  • 여러 앱에서 재사용 가능

Apollo Client: 여러 Graph를 다루는 방식

도메인별 GraphQL 엔드포인트가 여러 개이므로, 클라이언트도 분리해 구성했습니다.

export const createApolloClients = (
  businessUri: string,
  providerUri: string
) => {
  const businessClient = new ApolloClient({
    link: authLink.concat(httpLink(businessUri)),
    cache: new InMemoryCache(),
  });

  const providerClient = new ApolloClient({
    link: authLink.concat(httpLink(providerUri)),
    cache: new InMemoryCache(),
  });

  return { businessClient, providerClient };
};

Next.js로 인증 처리

Next.js API route를 인증(토큰 갱신 등)에서 활용했습니다.

export async function POST(request: Request) {
  const { refreshToken } = await request.json();
  const newTokens = await refreshCognitoToken(refreshToken);

  return Response.json({
    accessToken: newTokens.accessToken,
    idToken: newTokens.idToken
  });
}

Next.js를 선택한 이유

  • 인증용 API routes
  • Docker 배포에 적합
  • Angular/React 모두에서 공유 가능
  • 마이그레이션 후반까지 이어갈 기반

마이그레이션 워크플로우(기능 단위 교체)

1) React로 새 기능 구현

export const Feature = () => {
  const { data, loading } = useQuery(GET_DATA_QUERY);

  if (loading) return <Spinner />;

  return (
    <Card>
      <CardHeader>
        <CardTitle>{data.title}</CardTitle>
      </CardHeader>
      <CardContent>
        {/* Feature implementation */}
      </CardContent>
    </Card>
  );
};

2) Web Component로 래핑

const FeatureWC = r2wc(FeatureWithApollo, {
  props: {
    apiUrl: 'string',
    userId: 'string'
  }
});

customElements.define('app-feature', FeatureWC);

3) Angular에서 사용

<app-feature
  [apiUrl]="apiUrl"
  [userId]="currentUser.id">
</app-feature>

4) Feature Flag로 전환

<app-feature *ngIf="featureFlags.useNewFeature"></app-feature>
<legacy-feature *ngIf="!featureFlags.useNewFeature"></legacy-feature>

5) 기존 코드 제거

  • 플래그 제거
  • Angular 컴포넌트 삭제
  • 서비스 정리
  • 테스트 업데이트

6개월 후 지표

  • 주요 기능 15개 마이그레이션 완료
  • 빌드 70% 빨라짐
  • 중복 코드 40% 감소
  • 신규 기능의 80%를 React로 개발
  • 마이그레이션 관련 사고 0건

마무리

레거시를 “한 번에 갈아엎기”로 끝내지 않아도 됩니다.

모노레포 + GraphQL + Web Components + Next.js 조합으로, 비즈니스는 계속 돌리면서 기능을 하나씩 교체할 수 있었습니다.

핵심은 기술만이 아니라 워크플로우·확신·개발 경험(DX) 입니다. 점진적으로 도구와 패턴을 도입하면서도, 즉시 가치를 만들고 다음 단계를 위한 기반을 쌓을 수 있습니다.

Next.js Finally Has Competition

(길이상 요약 번역)

요지

글은 2026년 2월 시점에서 Next.js 16(16.1.6)TanStack Start(RC) 를 “기능 나열”이 아니라 아키텍처·운영 리스크·비용·실무 DX 관점으로 비교합니다. 결론은 “유행”이 아니라 내 앱의 성격으로 선택하라는 것입니다.

1) ‘무조건 Next.js’ 답이 흔들린 이유

과거엔 React 프레임워크 추천 = Next.js로 끝나는 경우가 많았습니다.

하지만 App Router + RSC(React Server Components) 중심 모델로 가면서, 개발자가 자주 겪는 어려움이 생겼다고 말합니다.

  • 코드가 서버에서 도는지, 브라우저에서 도는지 경계가 눈에 안 보이면 깨지기 전까지는 알기 어렵다
  • 상호작용이 늘수록 "use client"가 늘고, RSC 이점이 줄어들 수 있다

또한 Next.js 16에서 캐시 기본값이 바뀌어(동적은 기본 request-time, 캐시는 use cache로 명시) 큰 개선이 있었지만, “서버/클라 경계 관리”라는 구조적 긴장은 남아 있다고 봅니다.

2) TanStack Start의 철학(명시성 + 타입)

TanStack Start는 TanStack Router/Vite 기반의 풀스택 React 프레임워크이며, 글은 다음을 장점으로 강조합니다.

  • 무엇이 서버에서 실행되는지 코드로 명시(server function)
  • 라우트 params/search params/loader data 등에서 강한 TypeScript 경험(검증 포함)
  • “보이지 않는 경계”를 줄여 디버깅 비용을 낮추는 방향

예: ‘서버에서만 실행’ 로직을 명시적으로 선언

const getStats = createServerFn({ method: 'GET' })
  .handler(async () => {
    return db.getStats()
  })

컴포넌트는 일반 React처럼 hooks를 그대로 쓸 수 있고, 데이터는 loader로 가져온다고 설명합니다.

3) SEO에 대한 오해

글은 “TanStack Start는 SEO가 안 된다”는 말이 이제는 맞지 않다고 주장합니다.

  • SSR 기본 제공
  • head/meta/OG/JSON-LD 같은 메타데이터를 loader data 기반으로 구성 가능
  • prerender/sitemap/HTTP 캐시 기반 ISR도 지원

단, Next.js의 강점도 분명히 적습니다.

  • next/image 같은 이미지 파이프라인
  • Partial Prerendering 같은 기능

즉 “SEO 때문에 무조건 Next.js”는 단순화일 수 있다고 말합니다.

4) 숫자(메모리/빌드/번들)와 운영 관점

글은 여러 GitHub 이슈·사례를 근거로 다음을 언급합니다.

  • Next.js 개발 서버 메모리 증가 이슈(여러 이슈에서 GB 단위 언급)
  • 컨테이너 환경에서 메모리 누수/증가 보고가 있다는 주장
  • Turbopack은 빠르며(2~5배 등) Next.js 16의 강점

TanStack Start는 Vite 기반이라 dev 서버가 상대적으로 가볍게 느껴질 수 있다는 식으로 비교합니다.

5) 보안(공격 표면) 관점

글은 RSC 프로토콜 같은 “추가 프로토콜 레이어”가 생기면 공격 표면이 늘 수 있다고 말합니다.

반대로 TanStack Start는 server function이 “일반 HTTP 요청/응답” 형태라 단순하다는 점을 강조합니다.

(※ 이 부분은 각 조직의 위협 모델/패치 속도/운영 성숙도에 따라 실제 판단이 달라질 수 있습니다.)

6) 비용(호스팅) 관점

  • Next.js는 어디서든 호스팅 가능하지만, Vercel에서 가장 매끈한 DX를 제공한다
  • 반면 Vercel 요금은 트래픽/빌드/팀 규모에서 부담이 될 수 있다
  • TanStack Start는 “Node가 도는 곳”이면 배포가 가능해 비용 통제가 쉽다고 주장

7) 결론: 선택을 가르는 5가지 질문(요약)

글이 제시하는 질문을 간단히 옮기면:

  1. 내 앱은 읽는(콘텐츠) 앱인가, 조작하는(인터랙티브) 앱인가?

  2. 팀 규모/DevOps 여력은 어떤가?

  3. 이미 TanStack 스택(React Query/Router 등)에 많이 투자했나?

  4. RC(릴리스 후보) 상태를 감당할 수 있나?

  5. 호스팅 비용을 얼마나 통제해야 하나?

이 질문에 답하면 “그때그때 다른” 결론이 나온다고 말합니다.

Experiments with CodeMirror: Building a code review tool

(길이상 요약 번역)

배경

작성자는 복잡한 웹 앱을 만들수록 “괜찮은 텍스트/코드 에디터”가 필요하다고 느꼈습니다. 그동안은 단순 <textarea>로 버텼지만, 이제는 문법 하이라이트, 멀티 커서 같은 기능이 필요해졌습니다.

많은 LLM이 Monaco를 추천하지만, Monaco는 크고(무겁고) 커스터마이즈가 쉽지 않다고 느꼈고, 더 모듈형인 CodeMirror 6를 시도합니다.

또한 요즘은 LLM이 문서를 통째로 재생성하거나(tool 기반 편집으로) 직접 수정하는 흐름이 많아져서, 사용자가 변경사항을 보고 각 변경을 수락/거절(accept/reject) 하는 “리뷰 모드”가 더 중요해졌다고 봅니다.

이 글은 CodeMirror 6과 @codemirror/mergeunified merge view를 이용해 “간단한 코드 리뷰(변경 수락/거절) 확장”을 만드는 방법을 다룹니다.

웹 에디터 옵션을 간단히 훑기

  • 그냥 <textarea>

    • 장점: 브라우저 기본 기능(복사/붙여넣기, undo/redo) 그대로
    • 작성자는 최소한 자동 높이 조절 정도는 자주 넣는다고 합니다
  • Ace / Monaco / CodeMirror 5 등

    • Ace: 오래된 유명 임베디드 에디터
    • Monaco: VS Code 기반, 기능 풍부하지만 번들 크기/커스터마이즈가 부담
    • CodeMirror 5: 성숙했지만 구조가 오래됨
  • CodeMirror 6

    • 완전 재작성, 상태(state)와 뷰(view) 분리, 확장(extensions) 기반의 모듈 구조
    • 다만 문서가 “예제는 많은데 핵심 개념을 단계적으로 설명하는 튜토리얼이 부족”하다고 느꼈다고 합니다

CodeMirror 6의 핵심 개념(요약)

최소 셋업

import { basicSetup } from 'codemirror'
import { EditorView } from '@codemirror/view'

const view = new EditorView({
  parent: document.body,
  doc: 'Start document',
  extensions: [basicSetup],
})
  • 에디터 내용은 view.state.doc.toString()으로 읽을 수 있음
  • 변경은 .value = ... 같은 방식이 아니라 transaction(트랜잭션)dispatch해서 반영

예: 전체 문서를 교체하는 변경

view.dispatch({
  changes: {
    from: 0,
    to: view.state.doc.length,
    insert: 'New content'
  },
})

상태는 불변(immutable)

글은 CodeMirror 6의 아키텍처를 “functional core, imperative shell”에 가깝다고 설명합니다.

  • EditorState: 문서/선택/메타데이터/확장 상태를 담는 불변 객체
  • EditorView: DOM 렌더링과 사용자 입력을 처리하고, 트랜잭션을 적용
  • Extensions: 동작을 바꾸는 주요 수단

unified merge view로 diff/리뷰 모드 만들기

기본 예시:

import { unifiedMergeView } from '@codemirror/merge'

unifiedMergeView({
  original: 'one\n2\nthree\n4',
  allowInlineDiffs: true,
})

이 뷰는 “현재 문서”와 “original(기준 문서)”의 diff를 보여주며, 사용자가 변경을 accept/revert 할 수 있습니다.

accept/revert 이벤트 감지

문서에 명확히 정리된 설명이 부족해서, 작성자는 소스 코드를 찾아 userEvent를 활용했습니다.

  • EditorView.updateListener로 트랜잭션을 보고
  • tr.isUserEvent('accept') 또는 tr.isUserEvent('revert')를 감지

예시:

EditorView.updateListener.of(update => {
  const tr = update.transactions.find(
    tr => tr.isUserEvent('accept') || tr.isUserEvent('revert'),
  )

  if (tr) {
    const remainingChunks = getChunks(update.state)?.chunks.length
    const eventType = tr.isUserEvent('accept') ? 'accepted' : 'reverted'
    console.log(`Chunk ${eventType}! ${remainingChunks} remaining.`)
  }
})

여기서 getChunks()는 현재 diff의 남은 덩어리(chunks)를 가져오는 데 쓰입니다.

Compartment로 리뷰 모드 토글하기(핵심 DX 포인트)

리뷰가 끝나면(남은 변경이 0개면) diff UI를 꺼서, 사용자가 평소 편집 모드로 돌아가게 만들고 싶습니다.

이를 위해 Compartment를 사용해 특정 extension 묶음을 런타임에 reconfigure(재구성)합니다.

개념:

  • 초기에는 unifiedDiffCompartment.of([])처럼 비워둠
  • 리뷰 모드 진입 시 unifiedMergeView(...)를 넣어 활성화
  • 리뷰 모드 종료 시 다시 []로 바꿔 비활성화

예시:

view.dispatch({
  effects: unifiedDiffCompartment.reconfigure(
    enabled ? [unifiedMergeView(...)] : []
  ),
})

‘리뷰 모드’의 상태를 저장하는 방법

작성자는 “리뷰 모드에 처음 들어갈 때”의 기준 문서를 저장해두었다가, 그 기준 대비 diff를 보여주고 싶었습니다.

그래서:

  • StateField로 original 문서를 저장하고
  • StateEffect로 setOriginalDoc 같은 업데이트를 처리합니다.

이후 transactionExtender로 트랜잭션을 가로채,

  • review-changes 이벤트로 리뷰 모드 진입(기준 문서 세팅 + diff 활성화)
  • accept/revert로 변경이 모두 처리되면 리뷰 모드 종료(기준 문서 초기화 + diff 비활성화)

라는 흐름을 구현합니다.

결론

이 글은 CodeMirror 6의 “불변 상태 + 트랜잭션” 모델과 @codemirror/merge를 결합해,

  • 외부에서 문서가 변경됐을 때
  • 사용자가 변경 덩어리를 하나씩 수락/거절할 수 있는
  • 그리고 변경이 다 처리되면 자동으로 리뷰 UI를 꺼주는

리뷰 경험을 만드는 방법을 보여줍니다.

특히 Compartment로 extension을 토글하는 패턴과, userEvent(accept/revert) 감지가 실무에서 바로 써먹기 좋은 포인트입니다.

What’s your zero-downtime deployment strategy for Next.js on AWS Lambda?

안녕하세요.

저는 현재 Next.js 15 + SST + OpenNext 조합으로 만든 트래픽이 큰 체크아웃(결제) 서비스를 AWS Lambda 위에서 운영 중인데요, 배포가 꽤 흥미로운(=까다로운) 챌린지가 되고 있습니다.

비슷한 구성에서 거의 무중단에 가까운 배포를 어떻게 달성하는지, 실무 경험을 듣고 싶습니다.

  • Blue-Green vs Canary 중 어떤 방식을 쓰시나요?

  • Lambda 버전(version) + weighted alias를 활용하시나요?

  • Redis/캐시/상태(state)는 어떻게 처리하시나요?

  • ISR, RSC payload, 혹은 정적 자산(stale assets) 관련 이슈가 있었나요?

  • 롤백은 실무에서 어떤 형태로 하나요?

실제 운영 패턴, 트레이드오프, 그리고 시행착오(lesson learned)를 공유해 주시면 감사하겠습니다.

Vercel build machine defaults to Turbo?!? (0.13$/min)

방금 Next.js 앱 때문에 Vercel Pro 플랜을 결제했는데, 첫 주간 결제 리캡을 보고 놀랐습니다. 빌드 minutes 비용이 20유로가 찍혀 있었어요.

좀 찾아보니, 지난주부터인지 Turbo 머신이라는 것을 기본으로 활성화해 둔 것 같더라고요. 이 머신이 기본 빌드 머신보다 1000% (10배) 더 비쌉니다.

이건 너무 ‘얄밉게’ 느껴집니다. 제가 10년 개발하면서 본 어떤 서비스도, 가장 비싼 머신/CPU를 기본값으로 쓰게 하진 않았거든요.

차라리 GitHub Actions로 옮길까 생각 중입니다. 이 Vercel 비용에 비하면 사실상 공짜 수준이니까요.

Separating UI layer from feature modules (Onion/Hexagonal architecture approach)

안녕하세요.

여러 도메인에서 NestJS 앱을 만들면서(마이크로서비스, 모듈러 모놀리스 등) 겪은 경험을 바탕으로 글을 하나 썼습니다.

저는 오랫동안 Onion/Hexagonal 아키텍처를 적용할 때 기능(Feature) 모듈을 아래처럼 구성해왔습니다.

/order (feature module)
  /application
  /domain
  /infra
  /ui

그런데 시간이 지나면서, UI 레이어를 기능 모듈 밖으로 완전히 분리하는 쪽으로 바뀌었습니다.

지금은 대략 이렇게 구성합니다.

/modules/order
  /application
  /domain
  /infra

/ui/http/rest/order
/ui/http/graphql/order
/ui/amqp/order
/ui/{transport}/...

이렇게 하면 기능 모듈은 더 “순수”해지고, 특정 전송 계층(HTTP/GraphQL/AMQP 등)에 종속되지 않게 됩니다.

  • 유스케이스(use case)는 HTTP, GraphQL, AMQP 같은 것에 의존하지 않음
  • 전송 계층(transport)은 유스케이스를 조합(composition)만 함

이 방식은 특히 아래 상황에서 잘 맞았습니다.

  • 멀티 트랜스포트(REST + AMQP + GraphQL)
  • 모듈러 모놀리스로 시작해 마이크로서비스로 발전한 경우
  • domain/application 레이어를 깔끔하게 유지하고 싶을 때

다른 분들은 어떻게 하시는지 궁금합니다.

  • UI를 feature module 안에 두시나요, 아니면 분리하시나요?
  • 이 구조에서 cross-module aggregation(모듈 간 집계/조합)은 어떻게 처리하시나요?

더 긴 글도 있지만, 여기서 접근을 서로 교환하며 토론해보고 싶습니다.

(원문에 포함된 링크: https://medium.com/p/056248f04cef/)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment