Next.js 프로젝트 Migration과 Refactoring 과정을 공유합니다.

valley
더핑크퐁컴퍼니 기술 블로그
10 min readAug 29, 2022

안녕하세요. 웹개발팀 밸리입니다.

이번 포스팅은 사내 version 9 Next.js 그리고 기타 지원이 중단된 라이브러리들로 구성된 프로젝트를 Migration하고 Refactoring 하는 과정을 담았습니다.

레거시에 대한 이야기

업무 일정에 맞추기 위해 바쁘게 일을 하다 보면, 시간이 지난 프로젝트들은 금방 레거시가 되어 있습니다. 특히 빠르게 업데이트되고 변화하는 프론트엔드 세계에서는 더욱 그러한 것 같습니다.

당장 문제를 일으키지 않기 때문에 묻어가는 경우도 많습니다.

그러다 보면, 꽤 오래된 레거시 코드들은 폭탄이 되어 있어 최대한 건드리지 않는 방향으로 가게 됩니다.

// 이 코드는 함부로 건들이지 말 것...
function legacyFunction() {
return '아주 중요한 데이터';
}

저 또한 오래된 코드들을 보고도 당장 해야 할 일이 많고 해당 코드들로 기능이 큰 버그를 일으키지 않는다면 눈감고 코드를 지나친 적이 많습니다.
하지만 잘못했다고 하는 것은 아닙니다. 현재 레거시가 되어 버린 그 코드는 작성할 당시에는 나름대로 최고의 방법을 선택한 코드였을테니까요.

어느 날 갑자기 리팩터링

어느 날 운영자분께 아주 간단한 기능 업데이트 요구사항을 받고, 오랜만에 프로젝트를 열어봤습니다.

해당 요구사항은 20분도 채 걸리지 않아 구현되었지만, 이미 12 버전을 달리고 있는 Next.js의 버전은 9버전에 멈춰있고, 각종 This package has been deprecated를 내뿜는 패키지들 그리고 눈에 보이는 과거의 내가 짠 코드들을 마주하니 지나치기가 어려웠습니다.

해당 프로젝트는 외부 사용자가 사용하는 서비스가 아닌, 사내 서비스 운영자분들이 관리하시는 사내 서비스였기에 이슈가 생겨도 바로 공유할 수 있어 비교적 큰 부담 없이 진행을 결정했습니다.

Analyzer 도입하기

리팩터링을 진행하면서 번들 크기도 줄이고 싶어, @next/bundle-analyzer를 도입해 최종 번들된 프로젝트 사이즈를 시각화해서 볼 수 있도록 했습니다.

// next.conofig.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
openAnalyzer: false,
})
module.exports = withBundleAnalyzer({})

parsed 값을 보면 됩니다.
전체 2.4MB 정도라 아주 큰 정도는 아니지만, 충분히 줄여볼 만한 것들이 보입니다.

Next.js, React 버전 올리기

우선 버전 9 에 머물러 있는 Next.js를 버전 12로 올렸습니다.
React는 버전 16이었기에 18로 올릴까 하다가 아직 완벽 지원되지 않는 라이브러리가 있을 수 있음을 고려해 17로 올렸습니다.
(아마 조만간 18로 올리게 될 것 같습니다.)

import React 제거

react 17로 버전업을 하면서 새로운 JSX Transform덕에 더이상 import React from ‘react’ 구문을 정의해줄 필요가 없어졌습니다.

따라서 모든 파일의 import React from ‘react’ 문을 제거했습니다.

Babel을 제거하고 SWC 도입

SWC는 Speedy Web Compiler의 약자로, 빠른 웹 컴파일러의 기능을 제공하는 툴입니다.

Next v12부터는 Babel을 사용하지 않고, Rust기반의 SWC를 완전 도입했기에 더욱 빨라진 속도를 체감할 수 있다고 합니다.

로컬에서는 3배 더 빠른 새로고침 속도를, 프로덕션에선 5배 더 빠른 빌드 속도를 보여준다고 합니다. 특히 Babel에 비해 Rust의 컴파일 속도는 무려 17배나 빠르다고 합니다.

next 공식 페이지

왜 SWC에서 더 빨라진 속도를 보여줄까요?

SWC는 성능 최적화를 위해 다양한 기법을 사용했지만, 가장 대표적인 이유는 Rust의 병렬처리라고 할 수 있습니다. 이벤트 루프 기반의 싱글 스레드 언어인 Javascript와 달리 Rust는 병렬 처리를 고려해서 설계된 언어입니다. 즉 동시에 여러 파일을 처리할 수 있습니다. 그래서 코어 개수가 늘어날수록, 큰 프로젝트일수록 성능의 차이를 확연하게 느낄 수 있습니다.

SWC를 적용시키기 위해 옵션을 추가하고 기존 Babel config 파일은 제거하였습니다.

// next.config.jsmodule.exports = {
swcMinify: true
}

로컬에서 빌드를 돌렸을 때도 무려 약 2배나 빨라진 빌드 속도도 확인할 수 있었습니다.

(전)✨ Done in 40.40s. 👉🏻 (후)✨ Done in 21.00s.

Modularize Imports

NextJS v12에서는 modularizeImports 를 제공합니다.

이게 어떻게 강력한지 설명해드리겠습니다. 예를 들어 아래와 같은 코드가 있다고 가정해 보겠습니다.

import { merge } from 'lodash'

해당 코드만 보면 merge만 불러온 것 같지만, 사실 내부적으로는 lodash의 모든 메서드를 불러온 것과 동일합니다. 즉 아래 코드랑 동일하다고 볼 수 있습니다.

import _ from 'lodash'

따라서 lodash를 쓸 때는 직접 import 해오는 방식을 사용하라는 글을 많이 보셨을 겁니다. (혹은 lodash-es)

import merge from 'lodash/merge'

하지만 이제 그럴 필요가 없습니다. nextJS에서 modularizeImports 옵션을 활용하면 됩니다.

experimental: {
modularizeImports: {
lodash: {
transform: 'lodash/{{member}}',
},
}
}

그럼 이제 빌드 타임에 아래와 같이 변환됩니다.

import { merge } from 'lodash' -> import merge from 'lodash/merge'

Moment.js 잘가

날짜 관련 라이브러리로 Moment.js를 채택하고 있었습니다.

하지만 지원이 중단되어 더 이상 업데이트되지 않고, 사이즈 또한 큰(트리쉐이킹이 지원되지 않습니다.) Moment.js를 보내줄 때가 온 것 같습니다.

Day.js와 date-fns 라이브러리 둘 중에 어떤 것을 도입할지 고민이 되었습니다.

글 작성일자 기준으로 npm trends에서의 다운로드 수는 date-fns의 인기가 더 높음을 알 수 있습니다.

Unpacked Size도 살펴볼까요?

Day.js 648KB

date-fns 6.61MB

Size는 Day.js가 훨씬 가벼운 것 같습니다. Day.js가 애초에 작은 이유는 기타 플러그인을 제거한 코어 기능만 가지고 있기 때문입니다.

하지만 date-fns는 “트리쉐이킹”을 지원합니다! 심지어 다양한 API들의 성능을 비교했을 때 date-fns가 Day.js보다 빠른 경우가 많았습니다.

해당 라이브러리들을 비교한 Repository를 공유합니다.

https://github.com/you-dont-need/You-Dont-Need-Momentjs

이렇게만 보면 date-fns의 장점이 더 많은 거 아닌가? 싶을 수 있겠습니다.

하지만 저는 최종적으로 Day.js를 선택했습니다.

이유는 다음과 같습니다.

  • Moment.js에서 마이그레이션을 진행해야 하는 환경에서 같은 기능의 같은 API를 제공해 주는 Day.js 를 사용하면, 더욱 안전하고 쉽게 마이그레이션을 진행할 수 있다.
  • locale를 포함한다면, date-fns가 결코 작지 않다.
  • 코어한 기능만 담겨 있기 때문에 불필요한 플러그인을 굳이 가져올 필요가 없다.

공식문서에는 아래와 같이 쓰여 있습니다.

If you use Moment.js, you already know how to use Day.js.

Moment.js를 사용한다면 이미 Day.js 사용법을 알고 있는 것입니다.

Moment.js에서 제공해주는 자주 쓰는 API 들은 Day.js도 지원해주기 때문에, 무리 없이 마이그레이션을 진행할 수 있었습니다. 시간이 비용인 개발팀에서 전체적인 성능에 차이가 미미하다면, 더 빠르게 작업할 수 있는 라이브러리를 선택하는 것이 좋다고 생각합니다.

단, Day.js는 Moment.js와 달리 Immutable하므로 마이그레이션 할 때 이 부분을 주의해서 작업해야 합니다.

// moment
const momentDate = moment('2022-09-01');
momentDate.add(1, 'day'); // 1일 추가
console.log(momentDate.format('YYYY-MM-DD')) // '2022-09-02'

// dayjs
const dayjsDate = dayjs('2022-09-01');
dayjsDate.add(1, 'day'); // 1일 추가(반영 X)
console.log(dayjsDate.format('YYYY-MM-DD')); // '2022-09-01'

여기까지만 작업해도 2.6MB -> 2.01MB로 줄어든 것을 보실 수 있습니다.

마치며

1. 변수 이름을 잘 짓자, 기능은 잘게 쪼개자.

해당 기능과 역할을 제대로 담아내지 못한 변수 때문에, 이게 뭐 하는 코드였는지 기억이 나지 않아서, 코드 내부 로직을 다시 파악해보아야 했습니다. 더군다나 여러 기능이 한 함수에 담겨 있는 경우에는 더욱 코드를 파악하기가 어려웠습니다.

2. 테스트 코드가 진가를 발휘했다.

전체 코드에 대한 테스트를 작성해놓은 것은 아니었지만, 복잡한 로직들에 대한 테스트 코드는 작성된 상태였습니다. 처음 테스트 코드를 작성할 때는 효율성이 떨어진다고 생각했었는데, 이번 리팩터링을 진행하면서 마음 편하게 코드를 수정할 수 있었습니다.
특히 유틸 함수들에 대한 테스트 코드를 꼼꼼하게 작성해두었더니, 리팩터링 하면서 실수했는데 미처 발견하지 못한 부분을 잡아줘서 굉장히 든든했습니다.

참고

--

--