Node.js, NestJS, Sequelize 업그레이드를 나눈 순서
Node.js 12→20, NestJS 7→10, Sequelize 5→6을 155개 파일에 걸쳐 바꾸고 설치·빌드·테스트·주요 API를 단계마다 검증한 기록.
이 글의 목차세 버전은 서로 다른 층을 바꾼다10
기존 회사 서버의 의존성을 확인하다가 Node.js 12, NestJS 7, Sequelize 5가 한 덩어리처럼 묶여 있는 것을 발견했다.
Node.js는 서버에서 JavaScript를 실행하는 런타임이고, NestJS는 서버 애플리케이션의 구조를 잡는 프레임워크다.
Sequelize는 JavaScript 객체와 관계형 데이터베이스의 테이블을 연결하는 ORM이다.
ORM은 SQL을 모두 문자열로 직접 쓰는 대신 모델과 메서드로 데이터를 조회하고 저장하게 도와주는 계층이다.
처음 받은 요청은 Sequelize를 5에서 6으로 올리는 일이었지만 실제 범위는 ORM 하나로 끝나지 않았다.
Sequelize를 사용하는 NestJS 연동 패키지, 모델의 TypeScript 타입, GraphQL과 Firebase Admin, 테스트와 빌드가 서로의 버전 조건을 물고 있었다.
한쪽만 최신으로 올리면 설치 단계는 지나가도 컴파일이나 실행 단계에서 다른 축이 어긋났다.
세 버전은 서로 다른 층을 바꾼다
Node.js 버전은 애플리케이션이 실행되는 JavaScript 엔진과 기본 API를 바꾼다.
NestJS 버전은 모듈, 의존성 주입, HTTP 처리 같은 애플리케이션 구조의 규칙을 바꾼다.
Sequelize 버전은 모델 정의와 데이터베이스 호출, 생성되는 SQL의 경계를 바꾼다.
온라인 쇼핑 API를 예로 들면 Node.js는 매장 건물의 전기와 설비, NestJS는 주문·상품·회원 부서를 나누는 운영 규칙, Sequelize는 창고 장부와 실제 재고 표를 연결하는 직원에 가깝다.
한 층의 규칙을 바꿨을 때 나머지가 기대하는 입력과 출력도 맞는지 확인해야 한다.
이 때문에 package.json에서 숫자 세 개를 바꾸고 서버가 켜지는 것만으로는 충분하지 않다.
10.4.7처럼 세 숫자로 된 SemVer(Semantic Versioning)는 앞에서부터 메이저·마이너·패치 버전을 뜻한다.
메이저 증가는 호환되지 않는 변경이 있을 수 있다는 신호이고, 마이너와 패치는 보통 기존 사용법과 호환되는 기능·수정을 뜻한다.
다만 번호만으로 실제 호환성을 보장하지는 않으므로 각 패키지의 변경 문서를 확인해야 한다.
peer dependency는 한 패키지가 직접 포함하지 않고 사용하는 애플리케이션 쪽에 특정 버전을 함께 설치하라고 요구하는 의존성이다.
NestJS 연동 패키지가 허용하는 NestJS·Sequelize 범위가 맞지 않으면 설치 경고나 런타임 오류가 생길 수 있다.
Sequelize의 dialect는 MySQL·PostgreSQL처럼 실제 연결할 데이터베이스 종류와 그 드라이버 구성을 가리킨다.
| 바뀌는 층 | 먼저 깨질 수 있는 곳 | 최소 검증 |
|---|---|---|
| Node.js 런타임 | 네이티브 모듈, OpenSSL, 빌드 도구 | 설치, 기동, 외부 연결 |
| NestJS 프레임워크 | 의존성 주입, Guard, interceptor | 빌드, 주요 HTTP API |
| Sequelize ORM | 모델 타입, 반환값, SQL | 조회·저장·트랜잭션 |
| 주변 패키지 | GraphQL, 인증, 로깅 | 해당 기능의 통합 경로 |
네이티브 모듈은 JavaScript만으로 작성되지 않고 운영체제와 CPU에 맞춰 컴파일되는 의존성이다.
Node.js 주요 버전이 바뀌면 예전에 만든 바이너리를 그대로 쓸 수 없는 경우가 있다.
설치 성공 여부만 아니라 실제 컨테이너와 같은 운영체제에서 다시 빌드하고 실행해야 하는 이유다.
오래된 버전은 그대로 둬도 이미 변하고 있었다
Node.js 12는 새 기능이 부족해서만 교체한 것이 아니었다.
Node.js 공식 릴리스 기록은 Node.js 12의 지원 종료일을 2022년 4월 5일로 표시한다.
지원 종료는 프로그램이 그날 갑자기 멈춘다는 뜻이 아니다.
공식 프로젝트에서 보안 수정과 버그 수정을 더 이상 제공하지 않기 때문에 시간이 지날수록 안전하게 조합할 수 있는 주변 라이브러리가 줄어든다는 뜻이다.
Sequelize 5도 기존 코드를 계속 실행할 수는 있었지만, 새 NestJS 연동 패키지와 타입 정의를 맞추려면 Sequelize 6으로 넘어가야 했다.
Sequelize 6 공식 업그레이드 문서에는 Bluebird 제거와 네이티브 Promise 전환, sequelize.import 제거, upsert 반환 형식 변경 같은 호환성 주의점이 정리돼 있다.
이 항목이 우리 코드에서 모두 문제였다고 말할 수는 없다.
대신 모델 정의, 저장소 호출, 테스트와 빌드에서 실제로 드러난 오류를 고치면서 공식 변경 목록을 빠뜨린 경로가 없는지 확인하는 체크리스트로 사용했다.
큰 업그레이드를 네 개의 축으로 나눴다
버전 숫자를 한 번에 바꾸고 오류를 하나씩 지우는 방법도 있다.
하지만 빌드 오류 하나가 Node.js 런타임 때문인지, NestJS 타입 때문인지, Sequelize API 때문인지 알기 어려워진다.
나는 변경을 런타임, 프레임워크, 데이터 계층, 주변 연동의 네 축으로 나눴다.
첫 번째 축은 Node.js 12에서 16, 18.20.5, 20으로 이어지는 런타임이었다.
두 번째 축은 NestJS 7에서 8과 9를 거쳐 10 계열로 이동하는 프레임워크였다.
세 번째 축은 Sequelize 5.21.5에서 6.37.5, sequelize-typescript 1.1에서 2.1.6으로 이동하는 데이터 계층이었다.
네 번째 축은 GraphQL, Firebase Admin과 NestJS 연동 패키지처럼 애플리케이션 가장자리에서 함께 움직여야 하는 의존성이었다.
단계는 단순히 버전을 작은 폭으로 올리는 순서가 아니었다.
각 단계가 끝날 때 다시 실행할 수 있는 검증 지점을 만드는 것이 핵심이었다.
설치가 끝났는지, TypeScript 빌드가 통과하는지, 단위 테스트와 주요 저장소 호출이 기존과 같은 결과를 내는지를 확인한 뒤 다음 축으로 넘어갔다.
이번 작업의 중간 버전은 실패가 생긴 구간을 찾기 위한 검증 지점 역할을 했다.
선택지는 동결, 일괄 전환, 단계적 전환이었다
첫 번째 선택지는 현재 버전을 고정하고 보안 위험을 다른 통제로 버티는 방법이었다.
서비스 수명이 아주 짧거나 즉시 폐기할 시스템이라면 비용을 비교할 수 있지만, 계속 운영할 핵심 서버에는 후속 업데이트가 막히는 문제가 남았다.
두 번째 선택지는 모든 의존성을 최종 버전으로 한 번에 올리는 방법이었다.
작은 코드베이스에서는 빠를 수 있지만 이번 변경은 Git 기준 155개 파일에 걸쳐 있었다.
원본 기록에 남은 변경량은 추가 24,607줄과 삭제 22,745줄이었고, 이 숫자는 성능 향상치가 아니라 작업 범위를 보여 주는 보조 자료다.
세 번째 선택지는 실행 가능한 중간 조합을 유지하며 단계적으로 올리는 방법이었다.
작업 횟수는 늘지만 실패 지점을 좁힐 수 있고, 문제가 생기면 직전 검증 지점으로 돌아갈 수 있어 이 방법을 택했다.
단계적 전환도 중간 버전을 모두 운영에 배포한다는 뜻은 아니다.
로컬과 CI에서 호환성 경계를 건너기 위한 중간 지점으로 사용할 수 있다.
보안상 지원이 끝난 중간 버전에 오래 머물지 않고, 각 구간에서 무엇이 깨지는지 확인한 뒤 다음 단계로 이동한다.
실제 변경은 패키지 파일보다 넓었다
작업이 끝난 당시 핵심 의존성 조합은 다음과 같았다.
{
"@nestjs/common": "10.4.7",
"@nestjs/sequelize": "10.0.1",
"sequelize": "6.37.5",
"sequelize-typescript": "2.1.6",
"firebase-admin": "13.0.0"
}이 목록은 2024년 작업 완료 시점의 최종 스냅샷이다.
정확한 버전을 적은 이유는 당시 함께 시험한 조합을 재현하기 위해서였지, 2026년에 새 프로젝트가 그대로 설치해야 할 추천 목록을 만들기 위해서가 아니다.
현재 Node.js 공식 릴리스 표는 Node.js 20도 2026년 3월 24일 지원이 끝난 버전으로 표시한다.
따라서 이 글을 읽는 현재 시점에는 Node.js 20을 새 목표로 삼지 말고 지원 중인 LTS 버전과 사용하는 프레임워크의 호환 범위를 다시 확인해야 한다.
package.json을 고친 뒤에는 모델 속성과 저장소 호출의 타입 오류를 새 API에 맞게 수정했다.
타입 오류는 프로그램을 실행하기 전에 TypeScript가 값의 모양이 맞지 않는다고 알려 주는 신호다.
이 오류를 단순히 강제 형변환으로 숨기지 않고 실제 반환값과 모델 정의가 일치하는지 확인했다.
테스트 코드도 같은 이유로 함께 고쳤다.
테스트가 컴파일되지 않으면 운영 코드의 변경을 확인할 안전망 자체가 사라지기 때문이다.
NestJS는 주요 버전마다 마이그레이션 주의점을 문서화하고 있으며 현재 변경 기준은 NestJS 공식 마이그레이션 가이드에서 확인할 수 있다.
당시 7에서 10까지의 이동에서는 각 주요 버전의 릴리스 노트와 패키지의 호환 범위를 해당 단계별로 대조했다.
데이터베이스 변경과 라이브러리 변경을 구분했다
Sequelize 버전을 올리는 일과 데이터베이스 스키마를 바꾸는 일은 같은 작업이 아니다.
스키마는 테이블, 열, 인덱스처럼 데이터베이스 안의 구조를 뜻한다.
ORM 코드만 바뀌어도 생성되는 SQL과 반환값이 달라질 수 있고, 스키마 마이그레이션이 없어도 조회와 저장 경로는 회귀 시험이 필요하다.
반대로 테이블을 실제로 바꾸는 경우에는 up과 down처럼 적용과 되돌리기 절차를 별도로 준비해야 한다.
Sequelize 공식 마이그레이션 문서는 변경을 적용하는 up과 이전 상태로 되돌리는 down을 각각 정의하는 방식을 설명한다.
이번 작업은 런타임과 ORM 호환성 전환에 집중했고, 데이터베이스 구조 변경은 별도 배포 단위로 분리했다.
배포와 롤백도 같은 이유로 나눴다.
애플리케이션 코드만 바뀐 단계는 직전 이미지로 되돌릴 수 있지만, 데이터베이스 열을 삭제하거나 값을 변환한 뒤에는 코드 롤백만으로 원상복구되지 않을 수 있다.
먼저 새 코드와 옛 스키마가 함께 동작하게 만들고, 데이터 이동과 정리 작업을 뒤 단계로 미루면 되돌릴 수 있는 시간이 길어진다.
예를 들어 열 이름을 바꿀 때 바로 기존 열을 삭제하는 대신 새 열을 추가하고 한동안 두 형식을 읽도록 배포할 수 있다.
데이터를 옮기고 새 경로가 안정된 뒤 쓰기 경로를 전환하고, 마지막 배포에서 옛 열을 제거한다.
이 패턴은 이번 ORM 업그레이드의 확인된 변경 사항이 아니라, 스키마 변경이 함께 필요한 경우에 적용할 현재 권장안이다.
무엇을 통과해야 다음 단계로 갔는가
각 구간에서는 다음 순서를 반복했다.
- 목표 런타임에서 잠금 파일 기준으로 의존성을 설치한다.
- TypeScript 빌드를 실행해 모델과 프레임워크 타입 오류를 찾는다.
- 단위 테스트와 저장소 계층 테스트를 실행한다.
- 애플리케이션을 기동해 설정과 의존성 주입이 정상인지 확인한다.
- 주요 API와 데이터 조회·저장 경로를 시험한다.
- 통과한 버전 조합과 남은 오류를 기록한 뒤 다음 주요 버전으로 이동한다.
잠금 파일은 설치할 의존성의 정확한 버전을 기록한 파일이다.
같은 검증을 반복하려면 실행할 때마다 다른 하위 라이브러리가 설치되지 않아야 한다.
이 부분은 Docker 빌드 재현성: lockfile과 npm ci가 필요한 이유에 별도로 정리했다.
원본 기록으로 확인할 수 있는 것은 최종 의존성 조합과 155개 파일의 변경, 빌드·테스트·API 호환 수정 범위다.
응답 속도나 메모리 사용량이 얼마나 좋아졌는지에 대한 전후 수치는 남아 있지 않다.
그래서 성과를 “몇 퍼센트 빨라졌다”가 아니라 지원 가능한 생태계로 이동하고 후속 패치를 적용할 수 있는 기반을 만들었다고 표현했다.
다시 쓸 수 있는 주요 버전 업그레이드 체크리스트
- 현재 런타임과 모든 직접 의존성의 지원 종료일을 적는다.
- 런타임, 프레임워크, ORM, 데이터베이스 드라이버, 주변 연동을 별도 축으로 나눈다.
- 각 패키지의 공식 업그레이드 문서와 주요 변경 목록을 확보한다.
- 한 단계에서 바꿀 축과 통과해야 할 시험을 미리 정한다.
- 잠금 파일을 유지해 같은 의존성 조합을 반복 설치한다.
- 타입 오류를 강제로 숨기기 전에 실제 API 반환값이 달라졌는지 확인한다.
- ORM 코드 변경과 데이터베이스 스키마 변경을 별도 배포 단위로 판단한다.
- 최종 버전이 현재도 지원 중인지 게시 시점에 다시 확인한다.
- 변경 파일 수와 줄 수는 작업 범위로만 쓰고 성능 성과로 해석하지 않는다.
결론: 버전이 아니라 실패 구간을 관리했다
이번 작업은 Sequelize 5를 6으로 바꾸는 한 줄짜리 일이 아니었다.
Node.js, NestJS, Sequelize와 주변 연동을 서로 맞물린 축으로 보고 중간마다 실행 가능한 상태를 만들었다.
그 덕분에 155개 파일에 걸친 변경을 하나의 거대한 실패로 받지 않고, 어떤 버전 구간에서 생긴 문제인지 나눠 처리할 수 있었다.
다만 당시 도착점이었던 Node.js 20도 지금은 지원이 끝났으므로 현대화는 한 번의 프로젝트로 끝나지 않는다.
가장 오래 남은 결과는 특정 버전 숫자가 아니라 다음 업그레이드를 다시 나눠서 실행할 수 있는 검증 순서였다.