Angular 9에서 14까지, 5단계 점프 업그레이드 후기

Angular 9에서 14까지, 5단계 점프 업그레이드 후기

Angular 업그레이드, 해보신 적 있으신가요?

제가 회사에 입사했을 때, 이 프로젝트는 이미 Angular 9였습니다. 그리고 꽤 오랜 시간이 지났는데도 여전히 9였습니다. 잘 돌아가니까 굳이 건드릴 이유가 없었던 거죠. 다들 바쁘고, 업그레이드는 급한 일이 아니니까요.

그런데 개발을 하다 보니 불편한 점들이 하나둘 쌓이기 시작했습니다.


업그레이드를 결심한 이유

새 라이브러리를 쓸 수 없다

가장 답답했던 건 이거였습니다. 새로운 기능을 추가하려고 라이브러리를 찾아보면, 대부분 Angular 12 이상을 요구했습니다. peer dependency 충돌로 설치 자체가 안 되거나, 억지로 설치해도 런타임 에러가 발생했습니다.

기존 라이브러리도 문제였습니다. 버그 픽스나 새 기능이 나와도, 구버전 Angular를 지원하지 않아서 업데이트를 못 하는 경우가 잦았습니다. 분명 해결된 이슈인데 우리 프로젝트에서만 재현되는 상황이 반복됐습니다.

TypeScript 버전 제약

Angular 9는 TypeScript 3.8을 사용합니다. 다른 프로젝트에서는 더 최신 문법을 쓰는데, 여기서는 Angular 버전 때문에 TypeScript도 묶여 있었습니다.

버전이 너무 뒤처졌다

Angular 9는 2020년에 출시된 버전입니다. 최신 기능이나 성능 개선의 혜택을 전혀 받지 못하고 있었습니다.


이런 불편함들이 쌓이다 보니, "누군가는 해야 하는데"라는 생각이 들었습니다. 그래서 제가 직접 해보기로 했습니다.


업그레이드 범위 결정

원래는 최신 버전까지 올리고 싶었지만, 고객사 서버 환경이 Node 14로 제한되어 있었습니다. OS 관련 의존성 이슈라고 했습니다.

Angular 버전별 Node 지원 범위를 확인해봤습니다.

AngularNode 지원Node 14 호환
1414.15 - 16.x
1514.20 - 18.x
1616.14 - 18.x

Angular 15도 Node 14.20 이상이면 되긴 하는데, 고객사 환경이 정확히 몇 버전인지 확신이 없어서 Angular 14를 목표로 잡았습니다. Node 버전이 올라가면 그때 다시 시도해볼 생각입니다.

업그레이드의 핵심 목표를 두 가지로 정했습니다.

  1. 주어진 환경에서 가능한 최신 버전으로 올린다
  2. 새 라이브러리를 설치할 수 있는 환경을 만든다

업그레이드 전략

한 번에 여러 버전을 건너뛰면 어떤 변경이 어떤 에러를 유발했는지 추적이 어렵습니다. 그래서 한 단계씩 진행했습니다.

91011121314

매 단계마다 node_modules를 완전히 삭제하고 새로 설치한 뒤, 빌드와 실행을 확인했습니다.


단계별 기록

Angular 9 → 10

비교적 순탄했습니다.

안 쓰는 패키지가 발목을 잡다

npm ERR! peer @angular/core@"^9.0.0" from @ng-toolkit/universal@8.1.0

코드에서 찾아보니 실제로 안 쓰고 있어서 그냥 삭제했습니다. 누군가 예전에 추가해뒀다가 기능이 빠지면서 방치된 것 같았습니다.

없는 버전

npm ERR! notarget No matching version found for @nicky-lenaers/ngx-scroll-to@^10.0.0

npm에서 확인해보니 9.0.0 다음이 13.0.0이었습니다. 10, 11, 12가 없습니다. 일단 9.0.0으로 유지하고 Angular 13에서 올리기로 했습니다.

NgRx 빈 props

// 에러 발생
props<{ }>()

// 수정 - props가 필요 없으면 아예 생략
createAction("[Action] name")

Angular 10 → 11

TypeScript가 4.1로 올라가면서 타입 체크가 엄격해졌습니다.

// 에러: []는 never[]로 추론됨
getWishList: []

// 수정: 타입 명시
getWishList: WishItem[]

빈 배열 리터럴 []의 타입이 never[]로 추론되면서 생긴 문제입니다. 실제 사용되는 타입을 명시해서 해결했습니다.


Angular 11 → 12

이 단계가 제일 오래 걸렸습니다.

tslint 제거

Angular 12부터 tslint가 deprecated됐습니다. angular.json에서 lint 관련 설정을 제거했습니다.

extractCss 제거

// 삭제해야 함
"extractCss": true

Angular 12에서 이 옵션이 deprecated 되고 기본값이 true로 변경됐습니다.

--prod 플래그 변경

# before
ng build --prod

# after
ng build --configuration=production

CSS 파싱 에러

빌드는 됐는데 npm start에서 터졌습니다.

Error: Css Minimizer Plugin: Expected ")" to end URL token

원인은 SCSS에서 변수와 문자열을 연결하는 문법이었습니다.

// 에러
url($icons-image-url + "icon.png")

// 수정
url(#{$icons-image-url}icon.png)

Angular 12부터 들어간 esbuild 기반 CSS 처리기가 옛날 문법을 못 읽습니다. 8개 SCSS 파일을 전부 수정했습니다.


Angular 12 → 13

RxJS가 7로 올라갔습니다.

// RxJS 6: 됨
this.subject.next()

// RxJS 7: 안 됨, 값 필수
this.subject.next()  // error TS2554: Expected 1 arguments, but got 0

// 수정: void 타입으로 선언
private subject = new Subject<void>()
this.subject.next()  // ok

이 변경이 왜 필요했을까요?

Subject<any>.next()를 호출하면, 실제로 undefined가 emit됩니다. 받는 쪽에서는 any 타입을 기대하는데 undefined가 오는 거죠. 타입과 실제 동작이 불일치했습니다.

RxJS 7은 이걸 타입 시스템으로 강제합니다. "값을 안 보낼 거면 <void>로 명시해라." 덕분에 코드만 봐도 이 이벤트가 값을 전달하는지 아닌지 알 수 있게 됐습니다.


Angular 13 → 14

Router 옵션 변경

// deprecated
initialNavigation: "enabled"

// 수정
initialNavigation: "enabledBlocking"

내부 API import 차단

// Angular 14에서 막힘
import { Message } from "@angular/compiler/src/i18n/i18n_ast";

Angular 내부 경로(/src/)를 직접 import하는 게 막혔습니다. 확인해보니 실제로 안 쓰고 있어서 삭제했습니다.

Typed Forms

Angular 14의 가장 큰 변화입니다. FormGroupFormArray의 타입이 엄격해졌습니다.

// 타입 에러
control.setControl("field", new FormArray([]))

// 우회
(control as any).setControl("field", new FormArray([]))

제대로 고치려면 Form 인터페이스를 정의하고 모든 FormGroup에 제네릭을 적용해야 합니다. 일단 as any로 우회하고, 후속 작업으로 남겼습니다.

ng serve 옵션

// 삭제된 옵션
"--disableHostCheck=true"

// 대체
"--host 0.0.0.0"

결과

BeforeAfter
Angular9.1.1314.3.0
TypeScript3.84.7
빌드 시간약 2분 17초약 1분 26초

빌드 시간이 약 37% 감소했습니다. Ivy 컴파일러 최적화 덕분인 것 같습니다.


업그레이드 후 처리할 작업

업그레이드 과정에서 일부 이슈는 의도적으로 우회했습니다. 빌드를 먼저 성공시키고, 이후에 제대로 처리하기 위해서입니다.

이 목록은 팀에 공유해서, 업그레이드 완료 후 순차적으로 진행할 예정입니다.

1. @agm/core → @angular/google-maps 교체

@agm/core는 더 이상 유지보수되지 않는 라이브러리입니다. 공식 @angular/google-maps로 교체하면 peer dependency 충돌과 타입 에러(skipLibCheck)를 해결할 수 있습니다.

2. ESLint 설정

ng add @angular-eslint/schematics

현재는 린터 없이 빌드만 되는 상태입니다. 팀 컨벤션에 맞는 ESLint 규칙을 설정해야 합니다.

3. Typed Forms 적용

as any로 우회한 파일들에 Form 인터페이스를 정의하면 타입 안전하게 사용할 수 있습니다.

// 개선 예시
interface ItineraryForm {
  departure: FormControl<string>;
  arrival: FormControl<string>;
  date: FormControl<Date | null>;
}

// as any 없이 사용 가능
itineraryFormArray.push(this.fb.group<ItineraryForm>({...}));

마무리하며

5단계 업그레이드를 끝내고 나니, 업그레이드는 단순히 버전 숫자를 바꾸는 게 아니라는 걸 느꼈습니다. 각 버전에서 왜 이런 변경이 생겼는지 이해하면, 프레임워크의 방향성을 배우는 과정이 됩니다. RxJS 7의 타입 강화가 대표적입니다.

이 글이 Angular 업그레이드를 앞두고 있는 분들께 조금이나마 도움이 되었으면 좋겠습니다.