글 작성자: 택시 운전사
반응형

해당 글은 제가 DEVIEW 2020의 발표 영상 DEVIEW 2020 - React 개발이 이렇게 쉬웠나? (Feat. Next.js)을 보면서 문서화 및 제 의견을 추가한 글입니다.

0.React 개발이 이렇게 쉬웠나? (Feat. Next.js)

1. UI Library React / UI 라이브러리 리액트

리액트를 이용한 개발은 처음에는 모든 것을 해결해줄 만능 해결책을 가진 것 같이 느껴진다. 그러나 프로젝트의 규모가 커지고, 계속 사용할 수록 리액트를 이용한 개발의 어려운 점들이 등장한다. 1. UI Library React에서는 현재 리액트 개발이 어려운 이유와 이를 설명하기 위해 리액트와 리액트 개발에 대해 소개하려한다.

1.1 커져가는 React 생태계

npm trends - react vs. vue vs. angular vs. @angular/core

리액트는 꽤나 과거부터 2020년 현재까지, 주로 비교가 되는 다른 기술 스택들인 Vue.js, Angular.js와 비교해서, 프론트엔드 개발을 위한 기술 스택으로 압도적인 선두를 유지하고 있다. Vue.js의 스타 수가 더 많지만 이는 실제 사용보다 인기의 척도로 본다. 결국 중요한 건 얼마나 사용했는지 즉 다운로드 수이기 때문에 이를 기준으로 판단하였다.

The State of JavaScript 2019

또한, 그 해에 개발자들의 자바스크립트에 대한 생각을 조사하여 보여주는 The State of JavaScript 2019에서는 무려 71.7%의 사용자가 "이전에도 사용해왔고, 앞으로도 사용할 것"(I've USED it before, and WOULD use it again)이라는 응답을 하였다.

위에 언급한 동일 선상에서 비교되는 기술 스택인 다른 기술 스택들인 Vue.js, Angular.js를 표를 통해 비교해보면 비교해보면 다음과 같다.

특징 Angular React Vue
UI/DOM 조작
상태 관리
라우팅
양식 검증 & 핸들링
Http 클라이언트
  • View 레이어 초점을 맞춰 개발된다는 점이 리액트의 장점이자 단점이다.
  • 리액트는 UI라는 작은 영역만 담당하여 빠른 릴리즈가 가능하지만, Angular나 Vue 같은 Framework과 달리 전역에서의 상태관리나 클라이어트 사이드 라우팅(CSR, Client Side Routing)을 하려면 reduxreact-router-dom같은 추가 라이브러리 설치가 필수적이다.

1.2 React의 한계

위에 언급한 것처럼 리액트는 UI라는 작은 영역만 담당하기 때문에 프로젝트에 필요한 다양한 기능들을 사용하기에는 리액트가 가진 한계가 명확하다. 아래는 리액트가 가진 한계에 대한 내용들이다.

프로젝트 구성을 위한 기능들

현재 문제없는 협업을 위한 최신 스펙의 웹 프로젝트를 구성하기 위해서는 다음과 같은 많은 기능들이 필요하다.

  • SSR(Server Side Rendering) / SSG(Static Site Generation)
  • Type Checker
  • Routing
  • State management
  • Styling
  • Bundling
  • Lazy loading
  • Code splitting
  • Static file serve
  • Scroll restoration

아래 React Developer Roadmap 그림에서도 볼 수 있듯이 리액트 프로젝트를 제대로 공부하기 위해서는 다양한 분야를 알아야하고, 한 기능을 담당하는 라이브러리만해도 여러가지이기 때문에 이들의 장단점을 비교 분석해보기에는 시간이 너무 부족하게 된다.

React Developer Roadmap: Learn to become a react developer

리액트 빌드 툴체인 마스터

현재 속도 개선을 위한 기능들인 Bundling, Lazy Loading, Code Splitting 과 같은 기능을 지원하기 위한 Webpack과 Babel의 plugin, loader는 계속해서 늘면서 현재는 모두 약 5,000개로 이는 모두를 검증하여 사용할 대상을 결정할 수 있는 수준을 넘어선 정도까지 와버렸다. 이러한 지속적 복잡도 증가는 파편화된 생태계에서 프로젝트에 알맞는 라이브러리를 고르고, 이를 설정하고 유지보수하는 것 모두 일정이 촉박한 서비스 개발자에게는 매우 어려운 문제로 자리잡고 있다. 이러한 상황에서 서비스 개발자들은 리액트 빌드 툴체인 마스터가 되기보다는 규격화되어있는 프레임워크를 쓰는 방향으로 가고있다.

2. React Framework Next.js

2.1 Web Framework가 필요한 이유

UX(User Experience) vs. DX(Developer Experience)

사용자 경험(UX) > 개발자 경험(DX) > 구현 용이성

사용자 경험, 개발자 경험, 구현 용의성 중에서 결국 서비스를 사용하는 것은 사용자이기 때문에 사용자 경험이 가장 중요한 것으로 여겨지곤한다. 그러나 다른 한편으로는 개발자 경험 개선은 사용자 경험을 개선하는 가장 좋은 방법이라는 이야기도 될 수 있다. 예를 들어 빌드 시간 개선, HMR 제공등을 통해 DX가 개선된다면 UX의 영역에 속하는 버그 수정과 기능 개발에 걸리는 시간을 개선할 수 있기 때문이다.

그러나 반대로 개발자 경험은 낮아지지만, 사용자 경험이 높아지는 경우들도 존재하는데, SSR, Lazy Loading, Code Splitting등은 구현이 복잡하기 때문에 개발자 경험은 낮아지지만, UX인 성능 향상을 기대할 수 있다.

Web Framework의 구성 요소

웹 프레임워크의 구성 요소들은 모두 UX를 개선하거나, DX를 개선해서 UX를 개선하는 요소들로 구성되었다. 해당 요소들은 다음과 같다.

  • UI Component Model / UI 컴포넌트 모델링
  • Pre-rendering HTML / 프리렌더링 HTML
  • Routing / 라우팅
  • CSS Encapsulation / CSS 캡슐화
  • Data Flow / 데이터 플로우
  • Deployment/Releases / 배포/릴리즈
  • Compilation / 컴파일
  • Security / 보안
  • Bundling / 번들링
  • Backend/Server logic / 백엔드/서버 로직

2.2 Next.js vs. Gatsby vs. Create React App

현재 가장 대표적으로 사용되는 3가지 리액트 프레임워크는 다음과 같다. 이들의 장단점을 표로서 정리해보았다.

Next.js vs. Gatsby vs. Create React App

  Next.js Gatsby Create React App
장점 - 페이지마다 data-fetch, pre-rendering 전략을 다르게 가능
- 증분 정적 페이지 생성
- 다양한 상태관리, 스타일링 라이브러리 지원
- 파일 시스템 라우팅
- API route
- Webpack 5 지원
- GraphQL이 기본
- 플러그인 생태계
- 이미지 최적화
- 정적 페이지 성능이 next.js 보다 약간 좋음
-  가장 배우기 쉬움(추가 프레임 워크 없음)
- React 팀에서 생성 및 유지 관리
단점 - 프레임워크 지식 필요(_app.js, _document.js, getServerSideProps)
- 플러그인 시스템이 없음
- 이미지 최적화
- GraphQL이 기본
- 이미지 최적화와 정적 페이지 생성으로 빌드시간이 오래걸림(3000페이지 기준 30분)
- 서버 측 렌더링 없음(react-router 같은 서드파티 라우팅 라이브러리 필요)
- Code Splitting과 같은 기능을 직접 구현해야함
- eject시 모든 종속성을 관리해야해서 복잡성이 매우 증가함

 

Next.js vs. Gatsby vs. Create React App

세 프레임워크는 기능상 여러 차이점들이 있지만, 가장 큰 차이점은 렌더링 방식에 있다.

  • Create React App - CRA는 기본적으로 싱글 페이지 어플리케이션으로 CSR 방식으로 렌더링을 제공한다.
  • Gatsby - Gatsby는 Site Generator (정적 페이지)를 생성하여 SSG 방식으로 렌더링을 제공한다.
  • Next.js - Next.js는 SSG와 SSR 둘 다 사용 가능한 하이브리드 형태로 렌더링을 제공한다.

리액트 팀에서는 공식문서를 통해 각각에 목적에 따른 리액트 프레임워크를 제공하는 데 그 내용은 다음과 같다.

React 팀 추천 툴체인

 

새로운 React 앱 만들기 – React

A JavaScript library for building user interfaces

ko.reactjs.org

또한, Dan Abramov도 "static(SSG)", "server rendered(SSR)", "client rendered(CSR)" 셋을 혼합하는 현재 Next.js의 방식이 리액트 프레임워크가 나아가야 할 방향이라고 언급하기도 하였다.

Dan Abramov 트위터

네이버의 쇼핑 검색 서비스의 경우 처음에는 CRA(Create React App)eject하여 라이브러리를 적용하여 SSR이나 Code Splitting 같은 기능을 직접 구축하여 사용하다가 유지보수 측면과 여러가지 이슈로 인해 기존 프로젝트를 Next.js로 마이그레이션 작업을 진행하였다고 한다. 네이버 쇼핑 검색 서비스에서 리액트 프레임워크로 Next.js를 선택한 이유는 다음과 같다.

2.3 Next.js를 선택한 이유

Pre-rendering HTML

Next.js는 Pre-rendering HTML이라는 이름으로, SSR with (Re)hydrationCSR with Prerendering 방식을 혼합해서 사용하고 있다. 이러한 특성이 쇼핑 검색 서비스에 적합했던 이유는 다음과 같다.

  • 인터넷 쇼핑이라는 전자 상거래의 특성상 자주 컨텐츠가 바뀐다.
  • 네이버 쇼핑 검색을 사용하는 유저마다 최종적으로 렌더링되는 화면이 다른다.
  • 특정 쿼리를 기반으로 렌더링한다.

각 영역별로 SSR이 사용된 부분과 SSG가 사용된 부분은 다음과 같다.

SSR

SSR이 사용된 페이지는 쇼핑 검색 페이지, 쇼핑 카탈로그 페이지로 컨텐츠가 자주 바뀌는 대부분의 페이지에 사용된다.

쇼핑 검색 페이지

SSG

SSG가 사용되는 페이지는 쇼핑 카테고리 페이지, unsupport 페이지, 에러 페이지로 페이지 콘텐츠의 변화가 거의 없는 페이지에 사용된다.

쇼핑 카테고리 페이지

Next.js의 Pre-rendering 기능은 정적 페이지 자동화, 정적 페이지 생성, 증분 정적 재생성까지 가능한 현태이다.

Next.js의 사전 렌더링 히스토리

이러한 사전 렌더링은 초기에 로딩 화면 없이 페이지를 보여줌으로서 초기 로딩 성능과 SEO의 향상을 가져다주게 된다.

Next.js를 통해 구현할 수 있는 사전 렌더링의 두가지 형태인 SSR과 SSG를 표로 비교하면 다음과 같다.

SSR(Server Side Rendering) SSG(Static Site Generation)
- 페이지가 요청되었을 때 렌더링된다.
- 런타임 실행이 필요하다.
- 자바스크립트와 CSS 파일를 미리 가져오는 형태(Prefetch)
- 빌드타임에 렌더링된다.
- HTML이 CDN을 통해 제공된다.
- 자바스크립트와 CSS 파일 뿐만 아니라 데이터까지 미리 가져오는 형태(Prefetch)

Prefetch

Prefetch는 뷰포트에 나타나는 링크만 Prefetch(Intersection Observer API)하는 형태로 제공되며, 데이터를 동적으로 주입하게 된다. 네트워크 속도가 느리거나 Save-data가 on일 경우 disable되는 특성을 가지고 있다. Next.js는 Link 컴포넌트나 router의 prefetch 메소드를 통해 Prefetch 기능을 제공한고 있다.

// Link prefetch
<Link href="/search/category?catId=50000167" prefetch>
	<a>Blog Post</a>
</Link>
// router method
const router = useRouter( )
router.prefetch(`/catalog/[nvMid]`, `/catalog/${modelProduct.adId || modelProduct.id}`)

Prefetch의 예시 - 자동완성

getStaticProps, getStaticPaths (SSG)

getStaticProps와 getStaticPaths는 Next.js에서 SSG를 구하기 위한 메소드들이다. 해당 메소드들은 CDN에 캐싱할 수 있는 HTML과 JSON을 생성하게된다. 혹은 해당 프로젝트의 파일 시스템에서 직접 데이터를 읽어서 가져올 수도 있는데 이 경우에는 process.cwd()를 이용해서 데이터를 가져오게 된다. 또한, 위에서 언급했듯 렌더링에 필요한 해당 메소드들이 빌드 타임에 실행되며, 서버 측으로 번들린된다. 또한 최종적으로 결론적으로, Viewport 내에 링크가 있을 경우 링크에 해당하는 CSS, 자바스크립트, json 데이터를 미리 가져오게된다.

export async function getStaticProps() {
	const res = await fetch('http://.../posts')
    const posts = await res.json()
    return {
    	props: {
        	posts
        }, revalidate: 1 // In seconds 
    }
}

export default Blog

Viewport내에 링크가 있는 경우 CSS, JS, JSON을 prefetch

Incremental Static Regeneration

점진적 정적 재생성 기능은 Next.js에 20년 07년 도입되었으며, SSG의 단점인 초기 빌드 시간과 SSR의 단점인 초기 로딩 속도를 해결하기 위해 도입되었다. 트래픽이 들어올 때 백그라운드에서 다시 렌더링하여 기존의 페이지 정보를 업데이트하는 방식으로 요청 바로 이전에 데이터를 렌더링하는 특성을 가지고 있다. 만약 fallback: true를 주게 되면 로딩 후 실시간 렌더링하게 된다. 점진적 정적 재생성의 장점은 빠른 빌드 시간빠른 초기 로딩에 있다.

 

getServerSideProps(SSR)

getServerSideProps는 런타임에 실행되는 Next.js의 SSR 메소드이다. 서버 측에 번들링되며, Viewport 내에 getServerSideProps를 가진 페이지 컴포넌트에 대한 Link가 있을 경우, CSS와 JS를 prefetch 하고, 링크 클릭 시 getServerSideProps를 실행하여 json 데이터를 받아온다. 네이버 쇼핑 검새은 쇼핑 카테고리 영역에 해당 메소드를 사용하였다.

쇼핑 카테고리
CSS와 JS를 prefetch

쇼핑 검색의 경우 과거 CRA를 사용했을 때도 SSR을 구현했는데, 현재 Next.js를 이용한 구현과 비교하면 더욱 추상화 되고 간단해진 것을 확인할 수 있다.

Routing

Next.js는 파일 시스템 기반의 라우팅을 제공한다. 이러한 특징을 통해 라우팅 구축을 위해 react-router-dom같은 추가 라이브러리를 설치하거나 코드 작업을 할 필요가 없어지고, 정적, 동적, 중첩 라우팅 같은 복잡한 형태의 라우팅도 구현할 수 있도록 지원하고 있다. 또한, Shallow Routing을 이용하면 data-fetching 없이도 url을 변경할 수 있다.

Dynamic import

Next.js는 Chrome Webpack 코어팀과 협업하여 복잡한 구성없이 Dynamic Loading과 라우트 기반의 Code Splitting을 제공한다. 이를 통해 ES2020에 추가된 기능 중 하나인 Dynamic Import를 설정 없이 사용 가능하다. 또한 컴포넌트의 동정 로딩도 설정 없이 사용 가능하다.

 

Advanced Features: Dynamic Import | Next.js

Dynamically import JavaScript modules and React Components and split your code into manageable chunks.

nextjs.org

Code Splitting

Next.js는 페이지별 청크 분리를 통해 Code Splitting을 구현하고 있다. 개발자가 필요한만큼 common 청크를 생성(최대 25개)할 수 있으며, 자주 쓰이는 프레임워크(react, react-dom)의 청크를 둠으로써 캐시가 힛되는 비율을 높인다. Next.js의 Code Splitting 관련 내부 코드는 다음과 같은데 이렇게 복잡한 부분을 서비스 개발자가 일일히 다 찾아서 최적화하는 작업은 쉽지 않을 것이다.

Next.js Code Splitting 내부 구현 코드

Differential Loading

DifferentialLoading은 IE와 같은 레거시 브라우저와 Chrome 같은 현대 브라우저에 따라 번들링을 다르게 하는 방법이다. 해당 기능을 통해 브라우저에 맞는 번들을 보냄으로써 번들 사이즈와 불필요한 네트워크 비용을 줄여준다. 레거시 브라우저를 위한 폴리필도 설치 없이 기본적으로 제공되며 트랜스파일의 결과물도 분기처리를 통해 보여지게 된다.

개발 서버 최적화

DX를 높이는 기능중의 하나로, 개발자가 개발 서버를 띄울 때, 모든 페이지를 컴파일하는 것이 아닌 페이지에 액세스할 때 컴파일하는 기능이다. 웹소켓을 기반으로 5초마다 체크를 하며, 25초 동안 접근하지 않게 되면 캐시를 제거한다. 이를 통해 개발 서버를 띄우는 시간이 줄어들면서 개발자 경험이 좋아지게 된다.

Fast Refresh

Fast Refresh는 next.js, facebook 내부 프로젝트로 리액트 컴포넌트에 대한 변화로 인한 즉각적인 피드백을 준다. Next.js에서 기본적으로 사용 가능하며, Fast Refresh를 사용하면, 컴포넌트의 상태를 잃지 않고도 코드에서의 변화가 즉각적으로 보여질 것이다. 그러나 현재 클래스 컴포넌트나 HOC에서는 상태유지가 되지 않는다. 또한, 소스맵 자동 적용으로 IDE가 연동되어서 브라우저에서 에러를 클릭시 자동으로 IDE내의 에러 발생 부분으로 이동하게 된다.

Webpack 5 지원

Webpack5는 이전 버전 호환성을 지니며 Webpack 4와 아무 비용없이 마이그레이션이 가능하다. 뿐만 아니라 향상된 tree-shaking과 지속 캐싱, Deterministic chunk and module ids을 제공한다.

현재 nextjs.org와 vercel.com에서 사용중이다. CRA에서는 eject후 Webpack 3에서 Webpack 4 마이그레이션했을 때, webpack plugin/loader 관리, babel polyfill/plugin 관리, ts loader, style loader 모두를 웹팩에서 설정해야하는 등 리소스 낭비가 심했다.

ETC

  • 내장된 Styling(css module, css in js, sass, less, stylus) 지원
  • Data flow(redux, mobx, swr, apollo, recoil) 연동 등 210여개의 예제
  • Preview, Production, Serverless 배포 플랫폼
  • 내장된 TypeScript 지원
  • 정적 파일 서빙
  • API Routes
  • 절대 경로와 alias
  • Persistent Caching for Page Bundles
  • Rewrite, Redirect, Header
  • AMP 지원

Security & community

  • TypeScript 코드베이스
  • 2606 개의 통합 테스트, 191 개의 테스트 스위트
  • Google Chrome, React, Webpack 팀과 협업

Next.js in production

RFC

  • 이미지 최적화
    • 현재는 외부 플러그인을 통해서 가능
    • WebP, AVIF 같은 최신 형식으로 자동 변환
    • 이미지 사이즈 자동 조절
    • 로컬호스팅, CDN 이미지 경로 지원
    • preconnect, preload
    • 적은 시간 설치 가능

3. Troubleshooting & Tips

3.1 Troubleshooting

Mobile/PC Lighthouse 비교

Lighthouse를 사용해서 점수를 측정했을 때, SSG 시에는 문제가 없지만, SSR 시 모바일에서 점수가 낮은 경향을 보인다.

Query 기반 라우팅 불가

Query에 기반한 라우팅이 불가한 문제가 있었는데, 커스텀 서버를 사용하거나 9.5 버전에 추가된 redirect, rewrite을 사용해서 해결할 수 있었다.

Custom app에서 getInitialProps만 사용가능

  • automatic static optimization X
  • Redux 사용시 모든 페이지 reducer가 app.js에 번들됨 => code splitting 필요 혹은 다른 상태 관리 라이브러리를 사용해야 함

전역 css import 가 _app.js에서만 가능함

  • npm에 올려 사용하거나
  • styledjsx를 사용하면 됨

TypeScript barrel files에서 tree shaking 안 됨

  • sideEffects: false로 수정

CSS module dynamic import

  • v9.5.2 에서 CSS 모듈을 이용한 component dynamic import 에 FOUC 이슈
  • v9.5.3 으로 업그레이드 후에는 CSS preload CORS 이슈로 수정중

Shallow: true 옵션으로 페이지 이동 후 뒤로가기 시에 다시 데이터를 패칭하지 않음

this.props.router.beforePopState(({as, options}) => { if(options.shallow){ this.props.router.replace(as) return false } return true })

node_modules, monorepo의 local packages경우 transpile하지 않음

  • next-transpile-modules를 사용해야 함

IE11에서 개발서버가 실행이 안 됨(production에서는 가능)

  • ShadowDOM polyfill 추가

react-router와 다르게 history pushState기능이 없음

  • query를 사용하거나 브라우저의 Storage를 사용함

3.2 Custom Server

커스텀 서버를 사용하는 건 비추천한다. HMR을 .next를 제외하고 새로 구성해야 해야하는 문제가 있고, 서버에 맞는 tsconfig.json을 따로 설정해야했다. 또한, vercel로 배포를 원하는 경우에도, 커스텀 서버를 사용하는 경우 vercel을 통한 배포가 불가능한 문제가 있었다.

3.3 next.config.js

next.config.js는 Next.js에서 프로젝트 설정상 추가 구성이 필요할 경우 사용되는 설정 파일이다. 해당 파일은 서버에서만 실행되어 트랜스파일되지 않으며, phase-export, phase-production-build, phase-production-server, phase-development-server라는 총 4단계의 Phase로 구분된다. Webpack은 클라이언트와 서버에서 두 번 실행된다.

현재 실험하고 있는 기능은 다음과 같다.

  • Modern: 트랜스파일 차등 로딩
  • Profiling: 프로덕션에서 react profiling
  • productionBrowserSourceMaps: 소스맵 생성
  • scrollRestoration: 내부 링크에서만 가능
  • optimizeImages, Fonts: preload, preconnect

3.4 plugins

next-compose-plugins

플러그인을 사용할 수 있지만, 플러그인을 추가할 수록 깊이가 깊어져 코드를 제대로 읽기 힘들어지게 된다. 이 경우 next-compose-plugins라는 라이브러리를 사용하면 배열 형태로 plugin 을 추가할 수 있게 된다. 또한 optional로도 plugin을 주입할 수도 있다.

next-optimized-images

Gatsby.js와 달리 Next.js에 내장된 이미지 최적화 기능이 존재하지 않는다. 따라서 이를 위한 라이브러리가 필요한데 이 때 사용하는 것이 next-optimized-images이다. gatsby-image의 Next.js 버전으로 이미지 url에 쿼리를 추가하여 사용한다.

next-optimized-images

@next/bundle-analyzer

const withBundleAnalyzer = require('@next/bundle-analyzer')({
	enable: process.env.ANALYZE === 'true', 
})
module.exports = withBundleAnalyzer({})​

해당 라이브러리를 이용하여 번들을 분석한 이후 .next/analyze에서 분석한 html 파일을 확인 가능하다.

Review / 세 줄 요약

  • 현재의 리액트 개발은 여전히 쉽지 않음
  • 점점 복잡성을 줄이기위한 프레임워크들이 등장하고 있음
  • 리액트 프레임워크인 Next.js가 현재 선택할 수 있는 가장 최선의 선택
반응형