Frontend 면접 질문 목록, FRONT END 개발자가 알아야 할 N가지, 이런 목록에 꼭 빠지지 않고 보이는 항목은 SSR이다. SSR은 무엇이고 React에서의 SSR에 대해 정리해 보았다.
SSR은 서버사이드 렌더링을 의미한다. React를 기본 CRA로 사용하거나 처음 튜토리얼을 쭉 따라서 만들게 되면 CSR(Client-side-Rendering)을 한 것이다. SSR과 CSR의 차이는 렌더링의 책임이 어디에 있는지에 따라 나뉘게 된다.
CSR은 클라이언트에서, SSR은 서버에서 렌더링의 책임을 갖게 된다.
CSR의 React에서는 클라이언트에서 실행될 때 빈 html 파일을 구성하게 된다.
// App.tsx
export function App() {
return (
<div>
<Frame1 />
<Frame2 />
</div>
)
}
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>title name</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
↓
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>title name</title>
</head>
<body>
<div id="root">
<div>
<div class="frame1"> frame1 </div>
<div class="frame2"> frame2 </div>
</div>
</div>
</body>
</html>
CSR은 client에서 App.tsx
가 실행 될 때 html이 생성된다. 위의 빈 html에서 두번째 html처럼.
하지만 SSR은 완성된 두번째 html이 서버에서 넘어오게 된다.
그래서 완성된 html 파일을 제공해 준다는 것이다.
그렇다면 어떻게 SSR을 React에서 지원하는 걸까? jsp, ejb를 사용했을 때는 서버에서 html 사이사이에 직접 값을 넣어서 주는 방식이었다.
예)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>title name</title>
<% const data = getData(); %>
</head>
<body>
<div id="root">
<div>
<div class="frame1"> <%=data.frame1%> </div>
<div class="frame2"> <%=data.frame2%> </div>
</div>
</div>
</body>
</html>
라이브러리를 따로 사용하지 않았기 때문인데, React를 사용한다면 어떻게 React를 실행해서 넘겨 줄 수 있는 것일까.
세세한 코드보다는 크게 어떠한 순서로 또 어떤 이유로 SSR 환경구성을 진행 했는지를 정리해보았다.
전체 코드를 표현하지 않고 일부분만 표시, style 전달이나 apollo client 상태 전달에 대한 부분은 포함하지 않음.
React는 javascript에서 동작하는 라이브러리니까 서버에서 동작할 수 있도록 node로 서버를 구성하였다. 만약 서버 환경이 node가 아니라면 V8 엔진을 사용해서 React 렌더링 환경을 구축 할 수도 있다.
서버에서 렌더링하기 위해서는 서버용 Router 설정값과 초기 상태값을 전달해 주는 부분이 필요하다. 따라서 서버용 App.tsx를 분리해서 작성했다.
interface RenderI {
preloadState: string
location: string
client: ApolloClient<NormalizedCacheObject>
}
const RenderServerApp = ({ preloadState, location, client }: RenderI) => {
return (
<ApolloProvider client={client}>
<StaticRouter location={location}>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</StaticRouter>
</ApolloProvider>
)
}
export default RenderServerApp
StaticRouter
: 서버에서는 사실 Router주소가 변경되지 않으므로 StaticRouter
를 사용해서 라우팅한다. Client에서의 Router와 url과 context를 맞춰준다.
ApolloProvider
: Apollo client를 서버쪽에서 초기값을 넘겨주기 위해서 설정해서 넘겨줌
app.get(['*'], (req, res) => {
const renderedApp = renderToString(<RenderServerApp {...renderProps} />)
res.send(`<div id="root">${renderedApp}</div>`)
})
그럼 renderToString
은 Rendering된 초기 HTML 문자열을 반환한다.
// index.html
ReactDOM.hydrate(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root'),
)
hydrate
를 사용해서 받은 html에 이벤트를 연결한다.
코드가 전체 코드는 아니다.
하지만
1. 서버쪽에서 React 렌더링을 해야하기 때문에 서버에 node 환경이 필요하다.
2. 서버에서 렌더링 될 때 필요한 환경을 위해서 App.tsx(초기 렌더링 컴포넌트)를 분리해서 작성한다.
3. 그리고 그렇게 렌더링 한 React를 html로 전달한다.
4. 클라이언트에서는 html에 다시 이벤트를 연결하는 작업을 위해서 다시 렌더링 한다.
위의 네가지 부분이 처음에 이해할 때 가장 헷갈렸던 부분이다. React를 마치 JSP 처럼 처음 렌더링 해서 전달한다는 부분이 React가 CSR일 때 처음 렌더링 되는 부분을 생각하지 못했기 때문에 헷갈렸던 것 같다. (클라이언트에서 html을 구성하는 순서를 인지하지 못했음..)
프로젝트의 초기 구성에서는 관리자 페이지이기 때문에 초기 렌더링 속도가 중요하지 않다고 생각했기 때문에 CSR로 프로젝트를 진행했다. 하지만 추가적인 요구사항이 생기게 되면서 렌더링 속도를 고려하게 되었고 이를 대응하기 위해서 SSR을 적용하게 되었다.
SEO가 중요하지 않았기 때문에 Next.js를 고려하지 않았고, React SSR을 선택하게 되었다.
위에서 이야기했듯 서버에서 html을 구성해서 제공하기 때문에 클라이언트에서는 js를 받아서 react가 렌더링 될때까지 화면에 아무것도 표시되지 않는 CSR 보다는 빠른 초기 렌더링 속도를 확인할 수 있다.
따라서 렌더링 지표 중 FCP가 빨라지는 효과를 확인 할 수 있다. 하지만 이후 js를 다운받는 부분 이후에 상호작용이 가능하기 때문에 TTI속도는 줄어들지 않으며, 이 사이의 시간이 더 늘어나게 되는 부분은 적절하게 처리해야 한다.
ex) 로딩화면이나 화면상단과 하단의 스크립트를 분리해서 상단먼저 반응 가능하도록 처리하는 방식
FCP : 브라우저가 DOM 콘텐츠의 첫 비트를 렌더링할 때
TTI : Time to interactive: 사용자 입력에 안정적으로 반응할 수 있는 지점
SEO는 검색엔진 최적화로, 얼마나 검색엔진에 노출이 잘 되는지에 대한 부분이다. 사실 관리자 페이지였기 때문에 SEO를 중점에 두고 SSR을 진행하지 않아서 SEO에 대한 부분은 확인하지는 못했다. 하지만 기존의 CSR은 화면이 생성되어 있지 않기 때문에 검색엔진에 노출되는 부분이 더 불리하다고 알고있다.
막상 해보고 나니 헤멨던 시간보다는, 개념 자체는 어려운 부분이 아니었다는 생각이 든다. 다만 프로젝트 마지막 단계에 적용하다보니 다른 설정들을 맞춰주는 부분이 겹치면서 오래걸렸다.(ts, Apollo-client, styled-Component)
프로젝트를 설정할 때 초기 렌더링 속도 개선의 측면에서 효과가 좋았고, 설정만 한번 해 두면 쭉 편하게 쓸 수 있는 것 같다. 다음 개인 프로젝트에서도 SSR은 적용해서 프로젝트를 진행할 것 같다.