들어가기 전에

준비물

위의 준비물을 모두 갖췄다면, 다음을 터미널 환경에서 진행해보도록 하겠습니다.

$ npm install -g serverless
// 앞서 설치했다면 생략 가능하지만, 다시 설치해도 최신 버전으로 덮어쓰기 되니 
// 다시 입력해서 설치하셔도 무방합니다.
$ sls login
// sls는 서버리스의 축약어로, serverless login과 동일한 의미를 가지고 있습니다.

위 작업은 서버리스 프레임워크의 대쉬보드를 이용하기 위한 것입니다. 웹 브라우저를 통한 로그인이 되는 것만 확인하신 후 넘어가시면 됩니다. 추가적으로 샘플 서비스를 생성하여 간단히 서비스를 만들어(계정 생성 단계에서 진행) 보시는 것도 추천드립니다(과정이 영어라 거북하시다면 패스).

다음을 진행하기 위해 리액트 프로젝트를 하나 만듭니다.

$ npx create-react-app serverless
$ cd serverless

이후 이 영상을 통해 serverless framework에 우리의 aws 계정을 설정해주도록 합시다(자세한 가이드는 생략함).

설정이 끝났다면, 우리가 사용할 서버리스 프로젝트를 생성해주도록 합시다.

$ sls create --template aws-nodejs

그러고 나서는 이번 회차에서 간단하게 데이터를 받아 보여주기 위해 isomorphic-fetch 모듈을 받아줍니다.

$ yarn add isomorphic-fetch

serverless의 aws와 nodejs를 이용한 템플릿을 통해 우리가 사용할 서버리스 파일들을 가져옵니다(여담이지만 react는 nodejs에 종속되어 있기 때문에 serverless 프레임워크에는 aws-react 템플릿은 존재하지 않습니다(어차피 react 앱은 nodejs가 필요하기 때문). 만약 aws와 react를 같이 사용하는 템플릿을 사용하고 싶으시다면 따로 검색을 통해 사용하시길 바랍니다.

서버리스 번들링을 위한 밑작업 및 간단한 데이터를 호출해보자

다음으로 우리는 parcel을 통해 번들링(= 빌드)할 것이기 때문에 parcel 설정을 위한 parcel.js를 생성해 다음을 입력하겠습니다.

// serverless/parcel.js

const Bundler = require("parcel-bundler");
const Path = require("path");

// Entrypoint file location
const server = Path.join(__dirname, "app.js");

// Bundler options
const serverOpt = {
  outDir: "./", // The out directory to put the build files in, defaults to dist
  outFile: "handler.js", // The name of the outputFile
  publicUrl: "./", // The url to server on, defaults to dist
  watch: true,
  cacheDir: ".cache", // The directory cache gets put in, defaults to .cache
  minify: true, // Minify files, enabled if process.env.NODE_ENV === 'production'
  target: "node", // browser/node/electron, defaults to browser
  logLevel: 3, // 3 = log everything, 2 = log warnings & errors, 1 = log errors
  sourceMaps: true, // Enable or disable sourcemaps, defaults to enabled (not supported in minified builds yet)
  detailedReport: true // Prints a detailed report of the bundles, assets, filesizes and times, defaults to false, reports are only printed if watch is disabled
}

// Entrypoint file location
const browser = Path.join(__dirname, "./src/index.js")

// Bundler options
const browserOpt = {
  outDir: "./Browser",
  outFile: "bundle.js",
  publicUrl: "./",
  watch: true,
  cacheDir: ".cache",
  minify: true,
  target: "browser",
  https: false,
  logLevel: 3,
  hmrPort: 0,
  sourceMaps: true,
  hmrHostname: "",
  detailedReport: false
}

const serverBundler = new Bundler(server, serverOpt)
const serverBundle = serverbundler.bundle()

// Initialises a bundler using the entrypoint location and options provided
const browserBundler = new Bundler(browser, browserOpt)
const browserBundle = browserbundler.bundle()

서버를 위한 번들링 파일을 작성했다면, 다음으로는 우리의 CRA(create-react-app)기반 서비스를 조금 수정해야 합니다. 다음을 따라 각각의 파일을 수정합시다.

// serverless/package.json

{
  "name": "serverless",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.10.2",
    "react-dom": "^16.10.2",
    "react-scripts": "3.2.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "start:sls": "sls offline start",
    "build": "react-scripts build",
    "build:sls": "rimraf Browser && node parcel.js",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "deploy:sls":"sls deploy"
  },
  "babel": {
    "presets": [
      [
        "env",
        {
          "targets": {
            "browsers": [
              ">1%",
              "last 3 versions"
            ]
          }
        }
      ],
      "stage-2",
      "latest",
      "react"
    ],
    "plugins": [
      "syntax-dynamic-import",
      "transform-class-properties"
    ]
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
// serverless/src/index.js    

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import * as serviceWorker from './serviceWorker'

const rootElement = document.getElementById('root')

ReactDOM.hydrate(<App />, rootElement)

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
// serviceWorker.unregister()

/**
* ReactDom에서 제공하는 render와 hydrate의 차이는 따로 설명하도록 하겠습니다.
* 
* 기본적으로 제공되는 서비스워커(serviceWorker)를 사용하지 않는 이유는 우리는 서비스워커를
* 사용해 PWA를 만드는 것이 아니기 때문에 사실상 필요없는 로직이기 때문입니다.
*
**/
// serverless/src/app.js    

import React, { useEffect, useState } from 'react'
import logo from './logo.svg'
import './App.css'
import Users from "./apis/users"

const App = () => {
  const [users, setUsers] = useState([])
  useEffect(() => {
    const fetchUsers = async () => {
      const data = await Users()
      setUsers(data)
    }
    fetchUsers()
  }, [])

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
      <main>
        {users.map((user, i) => (
          <li key={i}>{user.name}</li>
        ))}
      </main>
    </div>
  )
}

export default App
// serverless/src/api/users.js    

import fetch from 'isomorphic-fetch'

const Users = () => fetch('https://jsonplaceholder.typicode.com/users')
    .then(data => data.json())

export default Users

여기까지 진행한 후 터미널에서 $ yarn start를 통해 다음처럼 제대로 users가 보이는지 확인해주세요. 현재는 서버 사이드 렌더링을 진행하지 않기 때문에 src/app.jsisServer에 대한 설정을 따로 언급하지 않고 넘어가도록 하겠습니다.

스크린샷 2019-10-09 오후 6.53.13.png


ReactDOM.render와 ReactDOM.hydrate의 차이를 알아보자.

위의 src/index.js에서 사용된 ReactDOM은 기본적으로 렌더링을 위해 render 함수와 hydrate 함수를 제공합니다.

render 함수는 첫 번째 인자로 리액트 엘리먼츠(elements, 여기서는 app)를 받고, 두 번째 인자로 제공된 컨테이너의 DOM(rootElement)를 받아 첫 번째 인자를 두 번째 인자에 렌더링하고 컴포넌트에 대한 참조(Ref)를 반환합니다.

또한, 리액트 엘리먼츠가 이전에 컨테이너 내부에 렌더링 되었던 적이 있다면 엘리먼츠를 업데이트하고 최신의 React 엘리먼츠를 반영하는 데 필요한 DOM만 변경합니다.

마지막 인자로 추가적인 콜백이 제공된다면 컴포넌트가 렌더링되거나 업데이트된 후 실행됩니다.

리액트 공식 문서에서는 다음과 같이 사용에 유의할 것을 권장하고 있습니다(일부 내용을 직접 읽고 이해한 것이기 때문에 정식 한글 번역 문서와 다를 수 있고, 의역이 있어 리액트 팀이 전달하고자 한 내용과 다를 수 있습니다).

ReactDOM.render() controls the contents of the container node you pass in.
Any existing DOM elements inside are replaced when first called. 
Later calls use React’s DOM diffing algorithm for efficient updates.

ReactDOM.render()는 유저가 전달한 컨테이너 노드(app)의 콘텐츠를 제어합니다.
처음 render 함수가 호출 될 때 기존 DOM 엘리먼츠는 교체되며, 
이후의 호출은 효율적인 업데이트를 위해 리액트의 DOM diifing 알고리즘을 사용합니다.

ReactDOM.render() does not modify the container node.
(only modifies the children of the container.)
It may be possible to insert a component to an existing DOM node, 
without overwriting the existing children.

ReactDOM.render()는 컨테이너 노드를 직접 수정하지 않습니다.
(오직 컨테이너의 자식 컴포넌트(children)만을 수정합니다.)
이런 방식은 기존 자식 컴포넌트를 덮어쓰지 않고, 
기존 DOM 노드에 컴포넌트를 삽입할 수 있을 것입니다.

ReactDOM.render() currently returns a reference to the root ReactComponent instance.
However, using this return value is legacy and should be avoided 
because future versions of React may render components asynchronously in some cases.
If you need a reference to the root ReactComponent instance, 
the preferred solution is to attach a callback ref to the root element.

ReactDOM.render()는 현재 루트(root) ReactComponent 인스턴스에 대한 참조를 반환합니다.
그러나 이 반환 값을 사용하는 것은 오래된 관습(legacy)이고 향후 버전에서 
경우에 따라 React가 컴포넌트를 비동기 적으로 렌더링 할 수도 있기 때문에 피해야 합니다.
만약 ReactComponent 인스턴스의 참조가 필요하다면 권장하는 해결책은 
callback ref를 루트 엘리먼츠에 붙이는(attach) 것입니다.

Using ReactDOM.render() to hydrate a server-rendered container is deprecated and will be removed in React 17. Use hydrate() instead.
ReactDOM.render()를 사용해 서버에서 렌더링한 컨테이너에 하이드레이트(hydrate)하는 것은 더이상 사용되지 않고, React 17 버전에서 삭제될 예정입니다. hydrate()를 사용해주세요.

마지막에 잠깐 언급된 것처럼 서버 렌더된 컨테이너를 하이드레이트(이하 hydrate)하기 위해서는 ReactDOM에서 제공하는 hydrate 함수를 사용해야 합니다. 그 전에 앞서 hydrate 무엇인지 알아보도록 하겠습니다.

hydrate는 일반적으로 '물을 포함한 물질'이라고 해석되며, 여기서 이 함수가 갖는 의미는(render가 rendering으로 해석될 수 있는 것처럼) hydrating에 가깝습니다. hydrating(수화)은 물을 포함한 물질을 물처럼 흐르는 속성을 부여한다고도 해석될 수 있으며, 리액트에서도 이와 마찬가지로 hydrate는 평문(Plane Text, 포맷없는 텍스트) 형태로 존재하는 state를 읽어들이는 행위를 나타냅니다.

글이 길어져 이번 회차는 여기까지 진행하도록 하고, 다음 회차에서는 만들어놓은 번들러(parcel.js)와 serverless 스크립트(start:sls, build:sls, deploy:sls)를 사용해 AWS Lambda 서버리스 환경에 배포해보도록 하겠습니다.