lechuck.dev

Phoenix 웹 애플리케이션에서 react.js 사용하기

마지막 업데이트:

Elixir 기반의 Phoenix 프레임워크를 사용해 웹 애플리케이션을 개발하는 경우, 기본적으로 LiveView 를 사용하도록 코드가 생성됩니다. LiveView 를 사용해 개발하는 것도 좋지만, React.js, solid.js 와 같은 라이브러리에 익숙하다면 같이 사용하는 것이 더 편할 수 있습니다.

Phoenix 를 사용해 웹 페이지를 만드는 경우, 각 웹 페이지는 서버에서 렌더링되어 클라이언트로 전달됩니다. 만일 CSR 방식을 사용해 react-router 와 같은 라이브러리를 사용해 클라이언트 라우팅을 사용하려면, Phoenix 로 생성한 프로젝트는 API 서버용으로만 사용하고 react 프로젝트를 별도로 생성하는 방법을 사용해야 합니다. 이 방법은 이 글에서는 다루지 않습니다.

이 글에서는 Phoenix 프레임워크를 사용해 웹 애플리케이션을 생성하고, react.js 를 같이 사용하는 방법과 배포하는 방법까지 알아보겠습니다.

  1. Phoenix 프로젝트 생성
  2. React.js 사용하기
  3. 도커 이미지 생성 및 실행

1. Phoenix 프로젝트 생성

Phoenix 프로젝트를 생성하려면, phoenix 개발에 필요한 프로그램들이 설치되어 있어야 합니다. 설치되어 있지 않다면 아래 섹션을 열어 필요한 프로그램을 먼저 설치해야 합니다.

필요한 프로그램 설치
# Elixir 설치
$ brew install elixir

# HEX (패키지 매니저) 설치
$ mix local.hex

# rebar (Erlang 패키지 매니저) 설치
$ mix local.rebar --force

# Phoenix 설치
$ mix archive.install hex phx_new

먼저 Phoenix 프레임워크를 사용해 프로젝트를 생성해 보겠습니다.

여기서는 Phoenix v1.7.2 를 기준으로 생성했습니다.

다음과 같이 실행하면 hello_umbrella/ 디렉터리에 hello 라는 이름의 프로젝트를 생성합니다.

$ mix phx.new hello --app hello --no-ecto --umbrella

생성된 프로젝트를 실행해보겠습니다.

$ mix phx.server

이제 http://localhost:4000 으로 접속하면 페이지가 뜨는 것을 확인할 수 있습니다.

애플리케이션을 IEx (Interactive Elixir)에서 실행할 수도 있습니다:

$ iex -S mix phx.server

디렉터리 구조

생성된 디렉터리 구조는 다음과 같습니다.

├── _build
├── apps
│   ├── hello
│   │   ├── lib
│   │   └── test
│   └── hello_web
│       ├── assets
│       ├── lib
│       ├── priv
│       └── test
├── assets
├── config
└── deps

2. React.js 사용하기

phoenix v1.7 부터 webpack 대신 esbuildtailwind 를 사용하도록 변경되었습니다.
esbuildassets/js/app.js 파일을 빌드한 결과물을 priv/static/assets/app.js 파일로 저장합니다.
개발 환경에서는 esbuild watcher가 이를 담당하며, 프로덕션 빌드에서는 mix assets.deply를 실행하면 생성됩니다.

react 디펜던시 추가

리액트를 사용하리면 먼저 리액트 관련 디펜던시를 설치해야 합니다.

$ cd apps/hello_web/assets
$ pnpm add --save-prod --save-exact react react-dom
$ pnpm add --save-dev --save-exact @types/react @types/react-dom

React 컴포넌트

apps/hello_web/assets/js/greeter.tsx 파일에 다음과 같이 간단한 리액트 컴포넌트를 생성합니다.

import React from "react";

interface GreeterProps {
  name: string;
}
const Greeter = (props: GreeterProps) => {
  return (
    <div>
      <h1>Hello {props.name}!</h1>
      <h1>Peace of mind with React.js and TypeScript.</h1>
    </div>
  );
};
export default Greeter;

리액트를 사용하려면 리액트 컴포넌트를 마운트하기 위한 컨테이너가 필요합니다.
apps/hello_web/lib/hello_web/components/layouts/root.html.heex 파일을 열고 <body> 부분에 greeting id 를 가진 <div> 엘리먼트를 추가합니다.

<body>
  <div id="greeting" />
</body>

마지막으로 컨테이너에 리액트 컴포넌트를 렌더링하도록 설정합니다.
apps/hello_web/assets/js/app.js 파일을 app.tsx 파일로 이름을 변경한 다음, 마지막에 다음과 같이 추가합니다.

import React from "react";
import { createRoot } from "react-dom/client";
import Greeter from "./greeter";

const container = document.getElementById("greeting");
if (container) {
  const root = createRoot(container);
  root.render(<Greeter name="Phoenix" />);
}

app.js 파일의 이름을 변경했기 때문에, 이를 참조하고 있는 esbuild 설정도 변경해야 합니다.
config/config.exs 파일을 열고 # Configure esbuild 섹션을 찾아서 아래 라인을 변경합니다.

# 변경 전
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
# 변경 후
~w(js/app.tsx --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),

마지막으로 개발 서버를 실행해 리액트 컴포넌트가 표시되는지, 파일 변경시 워치 모드가 잘 작동하는지 확인합니다.

$ mix phx.server

3. 배포용 도커 이미지 생성

Phoenix 애플리케이션 배포 준비는 크게 세 가지 단계가 있습니다.

3.1 애플리케이션 시크릿 처리

DB 사용자 이름과 패스워드와 같은 시크릿 정보는 일반적으로 환경 변수에 보관하고, 애플리케이션에서 로드하는 것이 좋습니다.
이 작업은 환경 변수에서 시크릿 구성 및 설정을 로드하는 역할을 하는 config/runtime.exs 파일에서 수행됩니다.

따라서, 프로덕션 환경에서는 적절한 관련 변수가 설정되어 있어야 합니다.

# 시크릿 키 생성
$ mix phx.gen.secret
<생성된 시크릿 키>
$ export SECRET_KEY_BASE=<생성된 시크릿>

# DB 사용시 DB 연결 정보 설정
$ export DATABASE_URL=ecto://USER:PASS@HOST/database

좀 더 자세한 내용은 Handling of your application secrets 문서를 참고하세요.

다음 단계를 수행하기 전에 프로덕션 환경에 필요한 디펜던시를 설치하고 컴파일하는 작업을 먼저 수행해야 합니다.

$ mix deps.get --only prod
$ MIX_ENV=prod mix compile

3.2 애플리케이션 asset 컴파일

이 단계에서는 esbuild 를 사용해 JS, CSS 과 같은 애셋들을 컴파일하고 번들링하는 작업을 수행합니다. 이 모든 작업은 mix assets.deploy 태스크 안에 캡슐화되어 진행됩니다.

$ MIX_ENV=prod mix assets.deploy

위와 같이 실행하면 apps/hello_web/priv/static 디렉토리에 배포용 번들 파일들이 생성됩니다.

생성된 애셋 파일들은 .gitignore 에 추가되어 있지 않으며, 개발자가 실수로 커밋해서 푸시하는 경우가 발생할 수 있기 때문에 apps/hello_web/.gitignore 파일을 열고 다음과 같이 추가하는 것을 추천합니다.

/priv/static/favicon-*.ico
/priv/static/robots-*
/priv/static/robots.txt.gz

혹은 mix phx.digest.clean --all 을 실행하면 apps/hello_web/priv/static 에 생성된 파일들을 모두 삭제할 수 있습니다.

3.3 프로덕션 환경으로 서버 시작하기

프로덕션 환경에서 서버를 실행하려면 mix phx.server를 호출할 때 PORT, MIX_ENV 환경 변수를 설정해야 합니다:

$ PORT=4001 MIX_ENV=prod SECRET_KEY_BASE=$(mix phx.gen.secret) mix phx.server

http://localhost:4001 로 접속해 페이지가 잘 뜨는지 확인합니다.

3.4 도커 이미지 빌드

Phoenix 에서는 도커 이미지 빌드에 필요한 Dockerfile 을 자동으로 생성하는 기능을 제공합니다. 하지만, 1.7.2 버전 현재 엄브렐러 프로젝트를 지원하지 않고 있어서 몇 가지 수정 사항이 필요합니다.

  1. apps/hello_web 디렉터리로 이동 후, 다음과 같이 실행하면 Dockerfile, .dockerignore 파일이 생성됩니다.

    $ cd apps/hello_web
    $ mix phx.gen.release --docker
  2. 실행시에는 엄브렐러 프로젝트 전체가 필요하므로 생성된 파일들을 엄브렐러 프로젝트 루트로 이동합니다.

    $ mv Dockerfile .dockerignore ../../
  3. 엄브렐러 프로젝트는 여러개의 애플리케이션을 포함하고 있기 때문에, 애플리케이션 별 릴리즈 설정을 추가해야 합니다. mix.exs 파일을 열고 project 정의 블럭안에 releases 항목을 추가합니다.

    def project do
      [
        apps_path: "apps",
        version: "0.1.0",
        start_permanent: Mix.env() == :prod,
        deps: deps(),
        aliases: aliases(),
        releases: [
          hello_umbrella: [
            applications: [
              hello: :permanent,
              hello_web: :permanent
            ]
          ]
        ]
      ]
    end
  4. config/runtime.exs 파일을 열고, 다음 항목을 찾아 주석을 해제합니다.

    config :hello_web, HelloWeb.Endpoint, server: true
  5. 프로덕션 배포용 릴리즈를 빌드합니다.

    $ MIX_ENV=prod mix release
  6. 생성된 Dockerfile은 루트 디렉토리에 있는 mix.exs, mix.lock 파일만 복사하고 있습니다. 엄브렐러 프로젝트에서는 각 애플리케이션의 mix.exs 파일도 같이 복사해야 합니다.

    COPY apps/hello/mix.exs ./apps/hello/mix.exs
    COPY apps/hello_web/mix.exs ./apps/hello_web/mix.exs
  7. 표준 Phoenix 프로젝트와 달리 우리는 리액트를 사용하기 때문에, npm 패키지가 설치되어 있어야 합니다.

    # npm 추가
    RUN apt-get update -y && apt-get install -y build-essential git npm \
        && apt-get clean && rm -f /var/lib/apt/lists/*_*
  8. Dockerfile은 루트 디렉토리에 있는 priv, assets, lib, rel 디렉토리를 복사하고 있기 때문에, 이를 변경해야 합니다. 또한, 빌드 전에 디펜던시 설치를 위해 npm install 을 실행해야 합니다.

    # 변경 전: COPY priv priv
    COPY apps/hello_web/priv apps/hello_web/priv
    
    # 변경 전: COPY lib lib
    COPY apps/hello/lib apps/hello/lib
    COPY apps/hello_web/lib apps/hello_web/lib
    
    # 변경 전: COPY assets assets
    COPY apps/hello_web/assets apps/hello_web/assets
    
    # 추가
    RUN cd apps/hello_web/assets && npm install
    
    # 변경 전: RUN mix assets.deploy
    RUN cd apps/hello_web && mix assets.deploy
    
    # 변경 전: COPY rel rel
    COPY apps/hello_web/rel apps/hello_web/rel
  9. Dockerfile에서 릴리즈된 파일들의 디렉토리 이름이 hello_web 으로 되어 있는데, 실제로는 hello_umbrella 로 생성되므로 이를 변경합니다.

    # COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/hello_web ./
    COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/hello_umbrella ./
  10. 도커 컨테이너 실행시 런타임에 실행 인자를 전달할 수 있도록 하려면, CMD 대신 ENTRYPOINT를 사용하도록 변경합니다.

    # CMD ["/app/bin/server"]
    ENTRYPOINT ["/app/bin/hello_umbrella"]
  11. .dockerignore 파일에서 컴파일된 애셋 경로들을 변경합니다.

    /apps/hello_web/assets/node_modules/
    /apps/hello_web/priv/static/assets/
    /apps/hello_web/priv/static/cache_manifest.json
  12. 마지막으로 도커 이미지를 생성합니다.

    $ docker build -t hello .

3.5 도커 이미지 실행

실행시 SECRET_KEY_BASE 환경 변수를 주입해서 실행합니다.

$ docker run --rm -e SECRET_KEY_BASE=$(mix phx.gen.secret) -p 4002:4000 hello start

로컬에서는 mix 가 설치되어 있어서 위와 같이 시크릿 키를 생성했지만, 실제 프로덕션 환경에서는 시크릿 키를 별도의 위치(예: k8s secret)에서 불러와 사용하는 경우가 일반적입니다.

http://localhost:4002 로 접속해 페이지가 잘 뜨는지 확인합니다.

소스 파일

참고 문서