레거시 Angular 단계적 종료: Next.js, GraphQL, 그리고 Monorepo로 마이그레이션하는 방법 (대대적인 재작성 없이)

발행: (2026년 2월 13일 오전 09:26 GMT+9)
10 분 소요
원문: Dev.to

Source: Dev.to

Sunsetting Legacy Angular: How We're Migrating to Next.js, GraphQL, and a Monorepo (Without a Big Bang Rewrite)의 커버 이미지

Overview

만약 오래된 프론트엔드에서 작업해 본 적이 있다면 그 이야기를 이미 알고 있을 것입니다. 애플리케이션은 성장하고, 기능은 쌓이며, 마감일은 계속 다가오고, 어느 순간 기술 부채의 산 위에 앉게 됩니다.

바로 그때가 우리 상황이었습니다.

우리는 600개 이상의 컴포넌트를 가진 대규모 Angular 14 애플리케이션을 보유하고 있었으며, 단일 구조와 복잡성이 증가하면서 개발 속도가 느려지고 있었습니다. 완전한 재작성은 매력적이었지만, 동시에 위험하고 비용이 많이 들며 비즈니스에 큰 혼란을 초래할 수 있었습니다.

그래서 대대적인 재작성 대신, 레거시 코드를 점진적으로 교체할 수 있는 마이그레이션 전략을 설계했습니다. 사용한 기술은 다음과 같습니다:

  • Next.js
  • GraphQL federation
  • Monorepo 아키텍처
  • 프레임워크 간 다리 역할을 하는 웹 컴포넌트

이 글에서는 아키텍처, 마이그레이션 접근 방식, 그리고 지금까지 배운 교훈들을 살펴봅니다.

레거시 환경: 우리가 시작한 것

우리의 Angular 애플리케이션은 수년간 비즈니스의 핵심이었습니다. 다음을 처리했습니다:

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

잘 작동했고 가치를 제공했습니다. 하지만 점차 노후화되기 시작했습니다.

레거시 시스템의 주요 과제

  • Monolithic architecture637개의 선언된 컴포넌트가 있는 하나의 루트 모듈 때문에 코드베이스를 이해하기 어려웠습니다.

  • Manual dependency injection – 커스텀 HTTP 서비스가 60개 이상의 위치에서 수동으로 인스턴스화되어 Angular의 DI를 우회했습니다.

  • Tight coupling – 컴포넌트가 특정 API 형태에 직접 연결돼 있었습니다.

  • Limited reusability – UI 컴포넌트가 Angular 전용이어서 다른 곳에서 재사용할 수 없었습니다.

  • Slow builds – 앱이 커질수록 빌드 시간이 점점 늘어났습니다.

  • Technology debt

    - Angular 14
    - Bootstrap 4
    - jQuery dependencies

앱은 여러 환경(dev, test, uat, prod)에서 유기적으로 성장해 왔습니다. 전체 재작성에는 12~18개월이 소요될 것으로 예상되며, 비즈니스 위험도 크게 증가합니다. 따라서 우리는 보다 안전한 접근 방식을 모색해야 했습니다.

조각들이 어떻게 맞춰지는지

  • Angular는 레거시 UI를 계속 실행합니다.
  • 새 기능은 React로 구축됩니다.
  • React 앱은 웹 컴포넌트로 배포됩니다.
  • GraphQL은 프론트엔드와 백엔드 사이에 위치합니다.
  • Next.js가 인증을 처리합니다.

이를 통해 비즈니스를 방해하지 않고 기능을 하나씩 교체할 수 있습니다.

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

우리는 Strangler Fig 패턴을 채택하여, 기존 시스템이 계속 가동되는 동안 점진적으로 시스템의 일부를 교체했습니다.

우리 접근 방식은 세 가지 핵심 기둥으로 구성되었습니다:

  1. Monorepo 기반
  2. GraphQL‑ 기반 API
  3. 브리지 역할을 하는 Web Components

Turborepo를 활용한 Monorepo

우리는 pnpmTurborepo를 사용해 monorepo를 구축했습니다.

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

장점

  • 앱 간 공유 코드
  • 엔드‑투‑엔드 TypeScript
  • 빌드 속도 향상 (≈ 70 % 개선)
  • 원자적인 크로스‑스택 PR
  • 일관된 버전 관리

API 레이어로서의 GraphQL

단일 REST API 대신 도메인 기반 GraphQL 서비스를 만들었습니다.

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

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

장점

  • 명확한 도메인 분리
  • 독립적인 배포
  • 강력한 타입 지정
  • 효율적인 클라이언트‑주도 쿼리
  • Federation‑준비 아키텍처

Web Components: Angular 안에 React 삽입

우리는 Web Components를 사용해 React 기능을 Angular 앱에 삽입했습니다.

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);

성공 요인

  • 프레임워크에 구애받지 않는 UI
  • 점진적인 마이그레이션
  • 최신 React 패턴 활용
  • 여러 앱에서 재사용 가능

Apollo Client: 다중 Graph 통신

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

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,
  });
}

Why Next.js

  • 인증을 위한 API 라우트
  • Docker‑ready 빌드
  • Angular와 React 사이에서 공유
  • 마이그레이션을 위한 미래 보장

데이터 레이어: Prisma + SQL Server

@Injectable()
export class DataService {
  constructor(private prisma: PrismaClient) {}

  async getBusiness(id: number) {
    return this.prisma.business.findUnique({
      where: { id },
      include: {
        subscriptionPlans: true,
        locations: true,
      },
    });
  }
}

마이그레이션 워크플로우

단계 1: React에서 빌드

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

  if (loading) return null;

  return (
    <div>
      <h2>{data.title}</h2>
      {/* Feature implementation */}
    </div>
  );
};

단계 2: 웹 컴포넌트로 래핑

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

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

단계 3: Angular에서 사용

import '@company/wc-feature';

단계 4: 기능 플래그

(플래그 구현은 Angular 설정에서 처리되며, HTML 자리표시는 간결성을 위해 생략되었습니다.)

단계 5: 오래된 코드 제거

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

6개월 후 주요 지표

  • 주요 기능 15개 마이그레이션 완료
  • 빌드 속도 70 % 향상
  • 중복 코드 40 % 감소
  • 새로운 기능의 80 %를 React로 구축
  • 마이그레이션 관련 사고 제로

최종 생각

레거시 앱을 종료한다는 것이 위험한 재작성이라는 뜻은 아닙니다.
다음 요소들을 결합함으로써:

  • A monorepo
  • GraphQL
  • Web components
  • Next.js
  • Turborepo

…우리는 비즈니스를 원활히 운영하면서 점진적으로 현대화할 수 있었습니다.

스트랭글러‑피그 패턴은 실제로 효과가 있습니다. 모든 것을 재작성하는 것과 영원히 레거시와 함께 살아가는 사이에 중간 경로가 존재합니다.

기술 스택 요약

레거시

  • Angular 14
  • Bootstrap 4 + jQuery
  • REST API
  • 모놀리식 아키텍처

모던

  • React 18+, Next.js 15
  • NestJS + GraphQL
  • Prisma + SQL Server
  • Turborepo + pnpm
  • @r2wc를 이용한 Web components
  • Tailwind + Radix UI
  • TypeScript 전역 사용
  • GitHub Actions + Docker
0 조회
Back to Blog

관련 글

더 보기 »

AppSync 마스터하기: Unions 및 Interfaces

팀에 AppSync 및 GraphQL GQL 소개하기 팀에 AppSync를 도입하는 데는 어려움이 있습니다—특히 팀이 GraphQL GQL에 익숙하지 않은 경우에는 더욱 그렇습니다. As th...

Angular 21에서 Signal Forms

Angular 21 Signal Forms – 새로운 사고 모델 수년간 Angular 폼은 하나의 의미만을 가지고 있었습니다: FormGroup, FormControl, valueChanges, 그리고 AbstractControl 트리.