티스토리 뷰

# 시작

[나만의 블로그]에서는 SSR(Server Side Rendering)과 CSR(Client Side Rendering)을 적절히 섞어서 사용할 계획입니다.

검색에 노출되어야 하는 화면만 SSR을 수행하고 나머지는 CSR을 적용하게 됩니다.

 

`이번 포스팅에서는 Nashorn을 이용한 SSR을 적용합니다.`

였으면 좋겠지만... TypeScript, Apollo에는 처음 적용해보는데 이런저런 에러가 계속 납니다.

하나씩 해결하다 보니 제법 시간이 오래 소요되어 차차 해결해야겠습니다.

일단 현재 구조에서의 SSR 구현에 관해서만 간단하게 포스팅하겠습니다.

참고로 Nashorn은 jdk11부터 deprecated 되었습니다.


# 개념

@CSR (Client SIde Rendering)

클라이언트에서 페이지를 렌더링 하는 방식입니다.

최근 유행하는 SPA에서 일반적으로 사용하는 방식입니다.

첫 화면 로딩 때 필요한 자원을 전부 로드한 뒤 (대규모 애플리케이션인 경우 Code Splitting 기법을 적용하여 부분적으로 로드하기도 합니다.)

JS를 이용한 렌더링과 데이터 변경만을 통해 서비스하는 방식이죠.

이 방식의 대표적인 장점은 아래와 같습니다.

  • 화면 이동 시 화면 깜박임이 없다.
  • 자연스러운 사용자 경험을 선사.
  • 필요한 컴포넌트만 로드하여 사용 가능.

그리고 대표적인 단점은 아래와 같습니다.

  • 보안
  • 메모리 누수
  • SEO가 어렵다.

SEO(Search Engine Optimizer)는 검색엔진 최적화입니다. 검색 포털들은 보통 검색 봇들이 웹크롤링을 수행합니다.

블로그나 마케팅 등등 검색 포털에 노출되는 게 중요한 사이트는 이 검색 봇에 수집이 잘 되어야 합니다. 

그런데 SPA에 크롤링 봇이 수집하러 왔을 때 이런 소스만 덜렁 있습니다.

<body>
  <div id="root"></div>
  <script src="./index.js"></script>
</body>

검색 봇은 기본적으로 HTML 내용을 수집하기 때문에 사실 위 소스는 빈 페이지나 마찬가지입니다. 제대로 수집이 안 되겠죠. 

물론 구글의 검색 봇은 자체적으로 JS Engine이 있어서 상관없다고는 하지만 다른 포털 사이트들도 고려해야 합니다.

이게 바로 SPA가 유행하는 요즘도 SSR이 계속 언급되는 이유입니다. SSR은 SEO가 용이하거든요.


@SSR (Server Side Rendering)

용어 그대로 서버 측에서 페이지를 렌더링 하는 것입니다.

SPA 등장 이전에 쓰이던 전통적인 방식의 웹 애플리케이션은 SSR 방식인 경우가 대부분입니다.

이 방식의 대표적인 장점은 아래와 같습니다.

  • SEO 용이.
  • 화면 이동 시에 메모리 Refresh.

대표적인 단점은 다음과 같죠.

  • 화면 이동 시 화면 깜박임.
  • 잦은 서버 렌더링으로 인한 부하.

SSR을 하게 되면 페이지가 서버에서 이미 완성되어 오기 때문에 SEO가 용이합니다.

또한 새로고침이 되면서 메모리도 리프레쉬되기 때문에 메모리 누수에 대한 걱정도 덜 하죠.


# 준비

build.gradle에 아래와 같이 의존성을 추가합니다.

// Thymeleaf
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'

정적 자원 매핑 설정을 합니다.

com/graphql/blog/config/WebMvcConfig.java

package com.graphql.blog.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {

    // Cors 정책을 모두 허용으로 설정
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**");
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    }

}

@addResourceHandlers

/static/~ 의 요청을 classpath:/static/ 경로로 매핑합니다.


이제 index.html 파일을 생성합니다.

src/main/resources/templates/index.html

<!DOCTYPE html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <a id="goHome">home</a>
    <br/>
    <!-- (1) -->
    <div id="root" th:utext=${App}></div>

    <!-- (2) -->
    <script src="/static/react/dist/bundle.js"></script>
    <script>
    document.getElementById('goHome').addEventListener('click', function () {
      render();
    });
    </script>
  </body>
</html>

@(1)

최초 로딩 시 SSR 된 HTML이 삽입됩니다.

@)(2)

bundle.js이 클라이언트 측에 로드되면서 이후 작업은 CSR으로 처리됩니다.


# Nashorn 및 SSR 구성

src/main/resources/static/react/polyfill/nashorn.js

var global = this;
var self = this;
var window = this;
var process = {env: {}};
var console = {};
console.debug = print;
console.warn = print;
console.log = print;
console.dir = print;
console.error = print;
console.trace = print;

Nashorn Jvascript Engine의 구문 분석 시 필요한 전역 변수들을 선언합니다.


src/main/resources/static/react/polyfill/bundle.js

function render () {
  if( typeof window !== 'undefined' && typeof document !== 'undefined' ){
    document.getElementById('root').innerHTML = "Client Rendering";
  } else {
    return "Server Rendering";
  }
}

여기서는 샘플 코드를 작성하지만 실제로는 Bundle파일이 됩니다.

서버에서 렌더링 되면 `Server Rendering'이 화면에 나타나고

클라이언트에서 렌더링 되면 `Client Rendering`이 나타나게 합니다.


com/graphql/blog/config/React.java

package com.graphql.blog.config;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

@Component
public class React {

  @Value(value = "classpath:static/react/polyfill/nashorn.js")
  private Resource nashornPolyfill;

  @Value(value = "classpath:static/react/dist/bundle.js")
  private Resource bundleJs;

  public String renderInit() throws ScriptException, IOException, FileNotFoundException, NoSuchMethodException {
    ScriptEngine engine = this.getScriptEngine();
    this.loadFile(engine);
    Object App = this.invokeRenderFunction(engine);
    return String.valueOf(App);
  }

  private ScriptEngine getScriptEngine() throws IOException {
    ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
    return engine;
  }

  private void loadFile (ScriptEngine engine) throws ScriptException, FileNotFoundException, IOException {
    engine.eval (new FileReader(nashornPolyfill.getFile().getCanonicalPath()));
    engine.eval (new FileReader(bundleJs.getFile().getCanonicalPath()));
  }

  private Object invokeRenderFunction (ScriptEngine engine) throws ScriptException, NoSuchMethodException {
    Invocable invocable  = (Invocable) engine;
    Object App = invocable.invokeFunction("render");
    return App;
  }

}

@getScriptEngine

Nashorn 스크립트 엔진을 생성

@loadFIle

Polyfill, bundleJs을 로드합니다.

@invokeRenderFunction

render 함수를 실행해서 그 결과를 문자열로 가져옵니다.

React라면 react-dom/server의 renderToString 함수가 되겠죠.


com/graphql/blog/menu/main/MainController.java

package com.graphql.blog.menu.main;

import java.io.FileNotFoundException;
import java.io.IOException;

import javax.script.ScriptException;

import com.graphql.blog.config.React;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MainController {
  
  @Autowired
  private React react;

  @RequestMapping("/")
  public String main (Model model) throws ScriptException, IOException, FileNotFoundException, NoSuchMethodException {
    String App = react.renderInit();
    model.addAttribute("App", App);
    return "index";
  }

}

model객체에 렌더링 할 문자열을 넣습니다.


# 실행

이제 제대로 SSR이 적용되는지 확인해봅니다. 서버를 시작합니다.

$ gradle bootRun

 

http://localhost:8000로 접속하고 아래와 같은 화면이 나타나는지 확인합니다.

소스 보기를 했을 때 SSR이 된 것을 확인할 수 있습니다.




이제 화면에서 [home] 링크를 클릭한 후 정상적으로 CSR이 되는지 확인합니다.

문구는 제대로 변경되었네요. 그럼 소스 보기를 해봅니다.

CSR이 되었기 때문에 최초에 SSR 된 페이지와 소스가 동일한 것을 확인할 수 있습니다.


# 마치며

[나만의 블로그] FE 포스팅에서 작성한 React 소스의 Bundle을 가지고 SSR을 못 끝낸 게 아쉽네요.

시간 날 때 적용해서 업데이트합니다.


# GitHub

https://github.com/eonnine/MyBlog.git

 

eonnine/MyBlog

Spring Boot, Graphql, PostgreSQL, React-Apollo, Parcel - eonnine/MyBlog

github.com

댓글