🔥💡 서버 사이드 렌더링 및 FE 용 서버 구축해보기
2022. 06. 20.
#개발환경
들어가기 전에
- FE 개발하면서 SPA, CSR, SSR 와 같은 개념을 많이 들었다.
현재 리액트를 많이 사용했기 때문에 SPA 와 CSR 을 직접사용하고 있는데 SSR 은 어떻게 동작하는지 알기 위해 간단한 데모용 서비스를 만들었다. (또는 Next.JS 와 같은 FE 용 서버?)
리액트로 직접해볼까 하다가 간단히 데모용으로 HTML 로 해도 리액트와 같은 방식으로 동작하기 때문에 정적인 HTML 기반으로 예제를 만들었다.
- 또한 리소스(이미지, HTML, JS 등) 에 캐시를 걸어 리소스 관리하는데 캐시를 적용하는 방법에 대해 기술하였다.
- SPA 와 CSR, SSR 에 대해 알고 싶다면 해당 링크를 참고하기 바란다.
프로젝트 구성도
SSR 예제를 만들기 위해 크게 2가지 서비스를 분리 시켰다.
하나의 서비스는 유저 서비스로 HOME(/) 과 ABOUT(/about) 인 경우로 생각했다. (해당 2개의 페이지는 하나의 SPA 로 구성)
또 다른 서비스는 Counter(/counter) 서비스로 해당 서비스에 접근할 경우 서버에 요청하여 접근할 수 있게 하였다.
⇒ 즉, HOME 에서 ABOUT 으로 전환될 때에는 SPA 방식으로 동작하지만, HOME 에서 COUNTER 로 전환될 때에는 COUNTER 페이지를 서버에 요청해서 접근한다.
또 COUNTER 에서 HOME 이나 ABOUT 서비스로 전환될 때에도 HOME 페이지를 서버에 요청해서 접근한다.
즉 다른 서비스인 경우 서버에 해당 페이지를 요청하는 방식으로 구성되었다.
클라이언트 구조 및 설명
USER
user.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" href="./assets/style.css" /> <title>SSR - USER</title> </head> <body> <header></header> <main> <h1>HOME</h1> <img src="./assets/milk.jpg" alt="milk" /> </main> <script type="module" src="./user.js"></script> </body> </html>
user.js
const $header = document.querySelector('header'); const $main = document.querySelector('main'); $header.innerHTML = ` <header> <nav> <li><a href="/">HOME</a></li> <li><a href="/about">ABOUT</a></li> <li><a href="/counter">COUNTER</a></li> </nav> </header> `; $header.addEventListener('click', (event) => { if (event.target.tagName !== 'A') { return; } const $href = event.target.href.split('/'); const path = $href[$href.length - 1]; if (path !== 'counter') { event.preventDefault(); } if (path === '') { $main.innerHTML = ` <h1>HOME</h1> <img src="./assets/milk.jpg" alt="milk" /> `; } else if (path === 'about') { $main.innerHTML = ` <h1>ABOUT</h1> <img src="./assets/olaf.jpg" alt="crayon" /> `; } });
- header 의 UI 를 렌더링하고 이벤트를 등록하였다.
- 이벤트에서 counter 를 클릭하지 않은 경우 (HOME, ABOUT 클릭 ) a 태그 기본 동작(요청 방지) 해서 SPA 로 동작하게 하였다.
만약 COUNTER 를 클릭 하면 a 태그의 href 가 정상 동작하여 /counter 로 요청될 것이다.
COUNTER
counter.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" href="./assets/style.css" /> <title>SSR - COUNTER</title> </head> <body> <header></header> <main> <h1>COUNTER</h1> <div class="counter"> <button id="increaseButton">+1</button> <span id="couterNumber">0</span> <button id="decreaseButton"">-1</button> </div> </main> <script type="module" src="./counter.js"></script> </body> </html>
counter.js
const $header = document.querySelector('header'); const $counter = document.querySelector('.counter'); const $number = document.querySelector('#couterNumber'); let number = 0; $header.innerHTML = ` <header> <nav> <li><a href="/">HOME</a></li> <li><a href="/about">ABOUT</a></li> <li><a href="/counter">COUNTER</a></li> </nav> </header> `; $header.addEventListener('click', (event) => { if (event.target.tagName !== 'A') { return; } const $href = event.target.href.split('/'); const path = $href[$href.length - 1]; if (path === 'counter') { // 1 event.preventDefault(); return; } // (2) if (path === 'about') { event.preventDefault(); } window.location = '/'; }); // counter 동작 이벤트 $counter.addEventListener('click', ({ target }) => { if (target.tagName !== 'BUTTON') { return; } if (target.textContent === '+1') { number += 1; } else if (target.textContent === '-1') { number -= 1; } $number.textContent = number; });
- header 의 UI 를 렌더링하고 이벤트를 등록하였다.
- (1) COUNTER 에서 counter 로 전환될 필요가 없어 해당 기능을 동작하지 못하도록 하였고
(2) counter 가 아닌 경우(home, about)
/
로 요청되게 하였다.(2) 에서 path 가 about 인 경우 event 를 방지하였는데 그 이유는 about 으로 요청하면 “/” 홈으로 요청해야하기 때문이다. 왜냐하면 HOME 과 ABOUT 이 하나의 서비스 “/” 에서 동작하기 때문에 “/about” 이 아닌 “/” 로 전환되게 설정하였다.
- 그 외적인 이벤트로 counter 기능이 동작하게 하는 이벤트를 등록하였다.
서버 구조 및 설명
- 해당 부분이 이번 주제에 핵심인 프론트엔드 용 서버를 구축하는 방법 또는 SSR 적용의 기본이 되는 부분이다.
구현
server > index.js
const express = require('express'); const path = require('path'); const app = express(); const header = { setHeaders: (res, path) => { // 캐시 적용 (이미지) if (path.endsWith('.jpg')) { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); // 캐시 적용 안함 // res.setHeader('Cache-Control', 'public, max-age:31536000'); // 캐시 적용 } }, }; // bun/ <- 얘만 걸림! // bun/category?tags=item app.use('/', express.static(path.join(__dirname, '../client/user'), header)); app.use('/counter', express.static(path.join(__dirname, '../client/counter'), header)); app.get('/', function (request, response) { response.sendFile(path.join(__dirname, '../client/user/user.html')); }); app.get('/counter', function (request, response) { response.sendFile(path.join(__dirname, '../client/counter/counter.html')); }); const http = require('http').createServer(app); http.listen(8080, function () { console.log('listening on 8080'); });
서버 코드를 보면
- header 를 정의해서 요청에 따라 주어진 리소스 에 따라 캐시를 적용했다.
(여기서는 jpg 이미지에만 캐시 적용)
- 요청이 오면 static 자원(리소스)을 바라볼 수 있도록 설정했다.
/
으로 오면 user 자원들을 응답할 수 있도록 설정/couter
로 오면 counter 자원들을 응답할 수 있도록 설정- 요청(
/
,/counter
)이 오면 그에 맞는 HTML 파일을 응답하여 서비스에 맞는 SSR 방식을 적용했다.
캐시 적용 전 동작 화면
해당 결과에서 2가지를 의도하였다.
- 캐시를 적용하지 않았기 때문에 같은 SPA 로 동작(HOME, ABOUT)하더라도 전환될 때 마다 이미지 요청
- USER 서비스와 COUNTER 서비스 전환 시 SSR 으로 동작하기 때문에 서비스에 맞는 HTML 요청 및 렌더
1. 캐시를 적용하지않았기 때문에 같은 SPA 로 동작(HOME, ABOUT)하더라도 전환될 때 마다 이미지 요청
- 이미지에 캐시를 적용하지 않았기 때문에
HOME → ABOUT
또는ABOUT → HOME
전환 시 이미지를 새로 요청하는 것을 볼 수 있다.
2. USER 서비스와 COUNTER 서비스 전환 시 SSR 으로 동작하기 때문에 서비스에 맞는 HTML 요청 및 렌더
HOME → ABOUT
또는ABOUT → HOME
전환 시 SPA 로 구성되어 있기 때문에 이미지만 요청되는 것을 확인 할 수 있다.
- 하지만
HOME → COUNTER
또는COUNTER → HOME
또는COUTNER → ABOUT
의 경우 다른 서비스 즉, SSR 로 전달되게 했기 때문에 관련된 리소스를 모두 요청하는 것을 확인할 수 있다.
캐시 적용 후 동작 화면
해당 결과에서 2가지를 의도하였다.
- 캐시를 적용했기 때문에 같은 SPA 로 동작(HOME, ABOUT)하면 최초 이미지를 제외하곤 요청하지 않음
- USER 서비스와 COUNTER 서비스 전환 시 SSR 으로 동작하기 때문에 서비스에 맞는 HTML 요청 및 렌더
2의 경우 캐시 전과 동일하기 때문에 여기서는 캐시 적용 후의 모습만 보여주고자 한다.
1. 캐시를 적용했기 때문에 같은 SPA 로 동작(HOME, ABOUT)하면 최초 이미지를 제외하곤 요청하지 않음
server > index.js
const header = { setHeaders: (res, path) => { // 캐시 적용 (이미지) if (path.endsWith('.jpg')) { // res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); // 캐시 적용 안함 res.setHeader('Cache-Control', 'public, max-age:31536000'); // 캐시 적용 } }, };
- 서버 header 응답 값으로
'Cache-Control', 'public, max-age:31536000'
을 설정하면 된다.
HOME → ABOUT
또는ABOUT → HOME
전환 시 이미지(jpg)에 캐시가 걸려 있어, 화면이 전환되더라도 이미지를 요청하지 않은 것을 볼 수 있다.
리액트는 어떻게 해요?
리액트에 적용하는 방법에 대해 궁금할 수가 있다. 하지만 별 거 없다.
해당 DEMO 예제 저장소 를 보면 위와 같다
client > counter
와 client > user
가 있는데 counter 와 user 가 react 를 빌드된 폴더를 지정하면 된다. 즉, CRA 로 생성한 counter 프로젝트를 빌드한 폴더가
client > counter
폴더로 대체하면 되고 CRA 로 생성한 user 프로젝트를 빌드한 폴더를
client > user
폴더로 대체하면 된다. 그리고 server 에서 코드의 하이라이트한 부분을 그에 맞게 설정하면 된다!
const express = require('express'); const path = require('path'); const app = express(); const header = { setHeaders: (res, path) => { // 캐시 적용 (이미지) if (path.endsWith('.jpg')) { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); // 캐시 적용 안함 // res.setHeader('Cache-Control', 'public, max-age:31536000'); // 캐시 적용 } }, }; app.use('/', express.static(path.join(__dirname, '../client/user'), header)); app.use('/counter', express.static(path.join(__dirname, '../client/counter'), header)); app.get('/', function (request, response) { response.sendFile(path.join(__dirname, '../client/user/user.html')); }); app.get('/counter', function (request, response) { response.sendFile(path.join(__dirname, '../client/counter/counter.html')); }); const http = require('http').createServer(app); http.listen(8080, function () { console.log('listening on 8080'); });
결론
- 서비스 리소스(Image, CSS, JS 등) 에 캐시하는 방법에 대해 알아봤다.
- SSR 방식과 클라이언트 용 Server 에 대해 아주 간단하게 알아봤다.
next.js 도 아마 기본적으로 이와 같이 동작하지만 다양한 기능들이 추가된걸로 판단된다!
- 개발하면서 SSR 과 클라이언트 용 서버가 무엇인지에 대해 궁금한게 있었는데 아마 이런식으로 동작하는 걸로 생각된다. 도움이 되었으면 좋겠다!
홧팅!!