오늘날 웹 어플리케이션 개발을 한다고 하면 대부분 리액트 앵귤러 뷰와 같은 자바스크립트 기반 프레임워크를 통해서 개발을 하게 됩니다.
MPA란
다양한 페이지를 만들어 매 요청마다 새로운 페이지를 요청해서 화면에 렌더링 해주는 전통적인 웹 방식이다
MPA의 단점
하지만 매번 새로운 HTML을 서버로부터 받아오고 전환 시마다 화면 깜빡임이 있다는 단점이 있습니다.
그 후로 AJAX라는 것이 나타나면서 동적으로 서버에서 원하는 정보를 받아와서 화면 깜빡임도 없는 SPA를 사용하게 되었습니다.
CSR && SSR
SPA는 웹 애플리케이션에 필요로 한 정적 리소스를 초반 한번에 모두 다운로드 하고 그 후 페이지 갱신이 필요로 할 때 필요로 한 데이터만 전달받아서 클라이언트에서 페이지를 갱신하기 때문에 자연스럽게 렌더링 방식으로 CSR을 사용하게 된다.
MPA는 새로운 요청이 있을 때마다 서버에서 이미 렌더링된 정적 리소스를 받아오기 때문에 렌더링 방식으로 SSR을 사용하게 됩니다.
CSR 작동방식
유저가 웹사이트에 방문하면 브라우저는 서버에서 콘텐츠를 요청하게 됩니다. 그 후 서버는 빈 뼈대 HTML과 연결되 JS링크를 제공하고 브라우저는 JS파일을 다운로드 하여 동적으로 DOM을 생성하게 됩니다. CSR은 이렇게 JS파일을 첫번째 입장시에 다운로드 받아 동적으로 DOM을 생성한다는 점에서 초기 로딩 속도가 느립니다. 하지만 초기 로딩 이후에 페이지를 변경할 때에는 서버에 데이터만 요청하면 되기 때문에 좋은 사용자 경험 ( 이후 좋은 구동 속도 ) 을 가질 수 있게 됩니다. 또한 서버는 빈 뼈대HTML만 넘겨주면 되기 떄문에 서버의 부하가 낮아지게 됩니다. 이밖에도 클라이언트 측에서의 연산 라우팅등을 모두 직접 계산하기 때문에 반응속도가 빠르고 UX가 매우 우수하다.
CSR 단점
하지만 웹 크롤러가 SEO최적화를 할 때 빈 뼈대 HTML만을 제공하므로 검색 가능한 색인을 만들 수 없어 SEO최적화에 매우 불리하다는 단점이 있다.
SSR 작동방식
유저가 웹사이트에 방문하면 브라우저는 서버에 콘텐츠 요청을 진행합니다. 서버에서는 렌더링 준비를 마친 HTML과 JS코드를 브라우저에게 직접 보내줍니다. 브라우저에서는 바로 전달 받은 페이지를 띄웁니다. 이어 브라우저가 JS코드를 다운받고 HTML에 JS로직을 연결합니다. 여기서 서버가 렌더링 준비를 마친 HTML을 브라우저의 응답으로 전달한다는 부분에서 모든 데이터가 HTML에 있기 때문에 SEO최적화에 유리합니다. 또한 JS코드를 다운받고 실행하기 전에 사용자가 화면을 볼 수 있다는 장점이 있습니다. 이로인해 모든 JS를 다 다운받아야 했던 CSR보다 초기 구동속도가 빠를 수 밖에 없겠죠? 하지만 TTV !== TTI 즉 Time To View와 Time To Interaction이 다르기 때문에 실제로 클라이언트 측 JS가 실행되고 이벤트 핸들러가 첨부되어서 JS로직이 모두 연결될때까지 사용자 입력에 응답할 수가 없습니다. 반면에 CSR은 JS가 동적으로 DOM을 생성하기 때문에 HTML은 JS로직이 모두 완전히 연결된 상태라 사용자가 보는 시점과 이용할 수 있는 시점이 동일하게 됩니다.
CSR의 단점 극복방법
CSR의 최대의 단점인 초기 로딩속도를 개선할 수 있는 부분이 있을까요? 우리는 code splitting, tree-shaking chunk분리등을 사용하여 CSR에서의 초기 로딩 속도를 보완할 수 있습니다. 또한 pre-rendering을 통해 각 페이지에 대한 HTML 파일을 미리 생성 해둔 뒤 서버에서 요청하는 자가 크롤러라면 사전에 렌더링 된 HTML버전 페이지를 보여 주는 방식을 통해 개선할 수 있습니다.
CSR + ( SSR + SSG )
또한 CSR을 사용하는 서버에 SSR이나 SSG를 도입하는 것도 CSR의 단점을 상당 부분 보완할 수 있습니다. 우리는 express.js나 typescript를 지원하는 nest.js를 사용하여 별도의 서버를 직접 운영하는 방법으로 CSR에서 SSR이나 SSG를 사용할 수 있습니다. 하지만 이 방법들은 서버 환경 구성이나 빌드 과정에 복잡하기 때문에 진입장벽이 꽤 있는 편입니다..
하지만 보다 쉽게 SSR이나 SSG를 적용할 수 있도록 해주는 프레임워크들이 있습니다.
먼저 Next.js는 리액트에서 SSR이나 SSG를 사용할 수 있게 해주는 프레임워크로 페이지의 성격별로 SSR를 사용할 것인지 SSG를 사용할 것인지 미리 적용할 것인지 정하는 것이 가능하게 됩니다.
또한 Gastby.js는 SSG에 최적화 된 리액트 기반 정적 페이지 생성 프레임워크로 SSG에 최적화되어 있긴 하지만 CSR, SSR, Lazy Loading을 지원하고 무엇보다도 굉장이 다양한 플러그인을 제공합니다.
CSR + SSR환경 구조
전체적인 구조는 다음과 같다. 도메인 네임을 통해 NginX 웹서버에 접근하면 NginX는 EC2 내부의 다른 포트에서 동작하고 있는 express서버에 접근한다. express서버는 일부 완성된 html문서를 NginX로 다시 되돌려주고 브라우저에게 그대로 전달한다. 이렇게 NginX를 한번 거쳐서 express같은 WAS와 연결하는 방식을 reverse proxy라고 한다. reverse proxty를 사용하는 이유는 여러 가지가 있지만, 외부 사용자들에게 WAS가 어떤 포트에서 동작하는지 숨겨 보안적으로 강화하기 위한 목적이 있다.
이런 흐름으로 사용자는 어느 정도 화면이 있는 html문서를 처음 응답으로 받아볼 수가 있는데, 비어있는 body를 첫 html응답으로 가져오는 CSR보다는 훨씬 빠른 화면 로딩 속도를 보여준다.
app.get('/', (req, res) => {
const indexFile = path.resolve(path.join(__dirname, '../client/index.html'));
const app = ReactDOMServer.renderToString(<App />);
fs.readFile(indexFile, 'utf8', (err, data) => {
if (err) {
console.error('Something went wrong:', err);
return res.status(500).send('Oops, better luck next time!');
}
const result = data.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
return res.send(result);
});
}
리액트를 사용한다면 개발자는 express.js에서 대략 위와 같은 코드를 작성하는데 ReactDOMServer.renderToString을 사용한다는 점을 빼면 CSR에서 렌더링하던 방식과 크게 다른 것은 없다. renderToString은 리액트가 컴포넌트 구조를 파악하고 HTML을 string으로 바꿔주는 동작을 한다.
이렇게 작업을 마치면 개발자 도구에서 html을 받아온 결과가 일부 완성된 것을 확인할 수 있다. 첫 html을 받아온 이 시점 이후부터는 더는 SSR이 아닌 CSR로 바뀐다. 페이지 이동도 SPA처럼 동작하는데 이게 어떻게 가능한 것일까?
일단 CSR + SSR환경이기 때문에 첫 html을 받아온 후에도 CSR환경을 동작시키기 위한 javascript를 불러오는 script태그가 여전히 들어가 있는 상태다. 그래서 html을 받아온 후 js파일을 가져와 CSR로 동작시키기 때문에 SSR에서 CSR로의 변경이 가능한 것이다.
그럼 여기서 의문점 하나가 들 것이다.
이렇게 되면 SSR에 의해서 일부 렌더링 된 화면을 CSR에 의해 다시 그리는 것이 아닌가? 엄청나게 비효율적일 것 같은데?
그 의문은 ReactDOM.hydrate()가 풀어준다. ReactDOM.render()메서드를 hydrate로 바꿔주기만 하면 리액트는 SSR에 의해서 일부는 렌더링 되어서 오는구나! 나는 나머지 부분만 채워주면 되겠네? 라고 생각한 뒤 이벤트 리스너만 등록하거나 렌더링되지 않은 나머지 부분을 추가 렌더링해 주는 등 효율적으로 동작한다.
'Web > NextJs' 카테고리의 다른 글
Next.js - Built-in CSS Support (0) | 2022.02.23 |
---|---|
Next.js - Data Fetching ( getStaticProps, getStaticPaths ) (0) | 2022.02.18 |
Next.js - Data Fetching ( getServerSideProps ) (0) | 2022.02.18 |
Next.js - Getting Started - 2 (0) | 2022.02.18 |
Next.js - Getting Started - 1 (0) | 2022.02.17 |