빌드 하셨어염?
우리는 리액트 프로젝트를 완성하고, 사용자에게 제공할 때는 빌드 과정을 거쳐서 배포를 하게 됩니다.
빌드를 하는 과정 속에는 자바스크립트 파일 안에 불필요한 공백, 주석, 경고메시 등을 제거해 파일 크기를 최소화하기도 하고 JSX 문법이나 최신 자바스크립트 문법이 원활하게 실행되도록 Babel을 활용해 트랜스 파일 작업도 할 수 있죠.
이 작업은 웹팩이라는 도구가 담당하고 있습니다. 웹팩에서 별도의 설정을 하지 않으면 프로젝트에서 사용 중인 모든 자바스크립트 파일이 하나로 합쳐지게 됩니다.
CRA(create-react-app)로 프로젝트를 빌드할 경우 기본 웹팩 설정에는 SplitChunks라는 node_modules에서 불러온 파일, 일정 크기 이상의 파일,
여러 파일 간에 공유된 파일을 자동으로 따로 분리시켜 캐싱의 효과를 제대로 누릴 수 있게 해줍니다.
캐싱의 효과가 어떤 느낌인지 와닿지 않을 텐데요.
먼저, npm run build를 통해서 빌드를 하면 아래와 같은 빌드 파일이 생성되고, 여러개의 js파일이 담긴걸 볼 수 있습니다.
이후 코드파일을 수정하고 다시 build하게 되면
아래와 같이 main 파일명이 수정된 걸 볼 수 있습니다. (이는 해시값이라고 합니다.)
따라서 브라우저가 파일명이 바뀌었는지 확인을 하면서 새로운 파일을 받아야할지 말아야할지 알 수 있습니다.
main에 담긴 코드들은 자주 바뀔 수 있는 코드들이고, 그 위에 목록된 js파일들은 코드가 잘 변하지 않는 코드들이 담긴다고 합니다.
이렇게 파일을 분리하는 작업을 코드스플리팅이라고 합니다.
잠깐, 코드 스플리팅이 뭔가요?
우리가 자바스크립트로 애플리케이션을 개발하게 되면, 기본적으로는 하나의 파일에 모든 로직들이 들어가게 됩니다. 그럼, 프로젝트의 규모가 커질수록 자바스크립트 파일 용량도 커지겠죠? 용량이 커지면, 인터넷이 느린 환경에서는 페이지 로딩속도도 느려질 것입니다.
코드 스플리팅을 하게 되면, 지금 당장 필요한 코드가 아니라면 따로 분리시켜서, 나중에 필요할때 불러와서 사용 할 수 있습니다. 이를 통하여, 페이지의 로딩 속도를 개선 할 수 있죠.
하지만 SplitChunks 웹팩 기능은 단순히 효율적인 캐싱 효과만 있습니다.
만약에 사용자가 A 라는 컴포넌트만 사용할 것인데, B와 C라는 컴포넌트 코드도 모두 main에 담기게 된다면 이는 효율적이지 못할 것입니다. 따라서 더 효율적인 코드 스플리팅이 필요하게 될텐데요, 필요할 때만 코드를 불러와 사용할 수 없을까요?
코드 스플리팅을 하는 방법 중 하나인 비동기 로딩에 대해서 알아보겠습니다.
● 자바스크립트 함수 비동기 로딩
먼저 실습을 위해 src 폴더에 notify.js라는 함수를 만들어보겠습니다.
경로 : src > notify.js
notify.js
export default function notify(){
alert('안녕하세요!');
}
그다음 이 함수를 불러와, 버튼을 누를때마다 함수가 실행되게 합니다.
경로 : src > App.js
App.js
import notify from './notify';
const App = ()=>{
const onClick = ()=>{
notify();
}
return (
<button onClick={onClick}>버튼</button>
)
}
export default App;
제대로 잘 실행이될텐데요, 다시 아래와 같이 수정해보겠습니다.
App.js
const App = ()=>{
const onClick = ()=>{
import('./notify').then(result => result.default());
}
return (
<button onClick={onClick}>버튼</button>
)
}
export default App;
import를 함수로 사용하면 Promise를 반환하게 됩니다. 이 문법은 아직 표준 자바스크립트가 아니지만 웹팩에서 지원을 하고 있기 때문에 프로젝트에서 바로 사용할 수 있습니다.
실제로 버튼을 누르게 되면, 누를때만 notify함수를 불러와 사용하는 것을 알 수 있습니다.
● React.lazy와 Suspense를 통한 컴포너트 코드 스플리팅
코드 스플리팅을 위해 리액트에 내장된 기능으로 유틸 함수인 React.lazy와 컴포넌트 Suspense가 있습니다. 이 기능은 리액트 16.6버전부터 도입되었는데요, 이전에는 import함수를 통해 컴포넌트를 불러온 다음 컴포넌트 자체를 state에 넣는 방식으로 구현해야했습니다.
먼저 실습을 위해 src 폴더에 컴포넌트 디렉토리를 만들고, SplitMe라는 컴포넌트를 만들어주었습니다.
경로 : src > components > SplitMe.js
SplitMe.js
const SplitMe = ()=>{
return (<div>SplitMe</div>)
}
export default SplitMe;
App 컴포넌트를 클래스형 으로 전환하고 handleClick메서드를 만들고 버튼을 누를때마다 SplitMe 컴포넌트를 불러와 state로 저장하게 만들었습니다.
App.js
import { Component } from 'react';
class App extends Component{
state = {
SplitMe : null // null값으로 먼저 지정
}
// 버튼을 누르면, import 함수를 사용해 컴포넌트를 가져와 state로 저장
handleClick = async () => {
const loadComponent = await import('./components/SplitMe');
this.setState({ SplitMe : loadComponent.default });
}
render(){
const { SplitMe } = this.state;
return(
<div>
<button onClick={this.handleClick}>버튼</button>
{ SplitMe && <SplitMe />}
</div>
)
}
}
export default App;
실제로 버튼을 누르면 SplitMe 컴포넌트가 보이는 것을 볼 수 있습니다.
하지만 이 방식은 매번 state로 선언해주어야한다는 점이 불편합니다.
❍ React.lazy와 Suspense 사용하기
React.lazy와 Suspense를 사용하면 state를 따로 선언하지 않고도 간편하게 컴포넌트 코드 스플리팅을 할 수 있습니다.
React.lazy는 컴포넌트를 렌더링하는 시점에서 비동기적으로 로딩할 수 있게 해 주는 유틸 함수 입니다.
// 예시코드
const SplitMe = React.lazy(()=> import('./components/SplitMe'));
suspense는 리액트 내장 컴포넌트로서 코드 스플리팅된 컴포넌트를 로딩하도록 발동시킬 수 있고, 로딩이 끝나지 않았을 때 보여줄 UI를 설정할 수 있습니다.
필수적으로 Lazy된 컴포넌트는 Suspense 내에 선언되어야합니다.
//예시코드
import { Suspense } from 'react';
(...)
<Suspense fallback={<div>loading</div>}>
<SplitMe />
</Suspense>
Suspense에서 fallback props를 통해 로딩 중에 보여줄 JSX를 지정할 수 있습니다.
다시 App.js로 돌아와 수정해보갯읍니다.
App.js
import React from 'react';
import { useState, Suspense } from 'react';
// 리액트 레이지 사용
const SplitMe = React.lazy(()=> import('./components/SplitMe'));
const App = ()=>{
const [visible, setVisible] = useState(false);
const onClick = ()=>{
setVisible(true);
}
return (
<div>
<button onClick={onClick}>벗흔</button>
// Suspense 컴포넌트 사용
<Suspense fallback={ <div>loading...</div>}>
{visible && <SplitMe />}
</Suspense>
</div>
)
}
export default App;
버튼을 누르면 로딩화면도 보이고, 로딩이 다되면 SplitMe 컴포넌트가 보이게됩니다. 또한 누를때 js파일이 로드되는 것을 확인할 수 있습니다. state로 지정한건 오로지 visible 밖에없어서 간편해졌습니다!
Suspense는 또한 여러 컴포넌트를 lazy로 불러온다고 했을 때, 구성 요소 가져오기가 완료되면 사용자는 동시에 표시되는 모든 구성 요소를 보게 됩니다. 즉 모든 요소들이 가져오기가 완료되어야지 보여집니다.
import React, { lazy, Suspense } from 'react';
const AvatarComponent = lazy(() => import('./AvatarComponent'));
const InfoComponent = lazy(() => import('./InfoComponent'));
const MoreInfoComponent = lazy(() => import('./MoreInfoComponent'));
const renderLoader = () => <p>Loading</p>;
const DetailsComponent = () => (
<Suspense fallback={renderLoader()}>
<AvatarComponent />
<InfoComponent />
<MoreInfoComponent />
</Suspense>
)
❍ Loadable Components를 통한 코드 스플리팅
React.lazy는 클라이언트 사이드 렌더링만 가능한 함수로, 서버사이드 렌더링에서 코드 스플리팅을 하고 싶다면 서드파티 라이브러리인 'Loadable Components' 사용해야합니다. 또한, 리액트 공식문서에서도 이를 권장하고 있습니다.
먼저 Loadable Component를 받아줍니다
npm i @loadable/component
lazy와 비슷하지만, Suspense를 사용할 필요가 없습니다.
App.js
import React from 'react';
import { useState } from 'react';
import loadable from '@loadable/component';
const SplitMe = loadable(()=> import('./components/SplitMe'));
const App = ()=>{
const [visible, setVisible] = useState(false);
const onClick = ()=>{
setVisible(true);
}
return (
<div>
<button onClick={onClick}>벗흔</button>
{visible && <SplitMe />}
</div>
)
}
export default App;
로딩 중에 다른 UI를 보여주고싶다면 아래와 같이 수정합니다.
const SplitMe = loadable(()=> import('./components/SplitMe'), {
fallback : <div>loading...</div>
});
...
그리고 컴포넌트를 미리 불러올 수도 있는데요, 아래와 같이 수정하면 마우스에 올리기만해도 컴포넌트를 미리 불러오게됩니다.
...
const App = ()=>{
const [visible, setVisible] = useState(false);
const onMouseOver = ()=>{
SplitMe.preload();
}
const onClick = ()=>{
setVisible(true);
}
return (
<div>
<button onClick={onClick} onMouseOver={onMouseOver}>벗흔</button>
{visible && <SplitMe />}
</div>
)
}
export default App;
● 총정리
따라서 서버 사이드 렌더링을 할 계획이 없다면 React.Lazy, Suspense로 구현하고, 서버사이드 렌더링을 계획한다면 Loadable Components를 사용하면됩니다!
이미지가 많이 담겨있는 컴포넌트가 토글된다던지, 컴포넌트를 숨겨두었다가 보여주는 형식으로 쓰이게 된다면 효율적일 것 같다는 생각이 들었습니다.
그리고 Suspense 같은 경우는 리액트 18버전 이후로 서버사이드렌더링을 지원할 수 있다고 합니다.