본문 바로가기

Study

[모던 리액트 Deep Dive 스터디] 모던 리액트 개발 도구로 개발 및 배포환경 구축하기

create-react-app / create-next-app

  • 애플리케이션 구축에 필요한 대부분의 작업을 대신 해줘서 편리하지만 프로젝트 구조를 이해하고 공부하는데 도움이 되지 않음.
  • create-react-app은 미래에 보일러 플레이트 CLI가 아닌 리액트 기반 프레임워크를 제안하는 런처 형태로 변경될 예정

위와 같은 상황으로 인해, 리액트 프레임워크 구축하는 방법을 알아야 한다.

create-next-app 없이 하나씩 구축하기

1. package.json 만들고 필요한 라이브러리 설치하기

npm init 을 사용하면 package.json을 만들 수 있다.

 

다음으로 Next.js 프로젝트를 실행하는 데 필요한 라이브러리 3가지를 설치한다.

npm i react react-dom next

 

그 다음 devDependencies에 필요한 패키지를 설치한다.

타입스크립트 사용에 필요 - typescript

타입스크립트 내부에서 리액트 타입 지원에 필요 - @types/react, @types/react-dom

Node.js의 타입을 사용하기 위해 필요 - @types/node

ESLint 사용에 필요 - eslint, eslint-config-next

그럼 기본적인 패키지 설치는 끝이 난다.

2. tsconfig.json 작성하기

이제 타입스크립트 코드를 작성하기 위한 준비가 필요하다.

npm 설정을 package.json에 하듯이 타입스크립트 설정은 tsconfig.json에 기록한다.

{
  "$schema": "<https://json.shemastore.org/tsconfig.json>"
}

 

먼저, tsconfig.json 파일을 생성해 JSON 상단에 위 코드를 추가해준다.

위 코드를 추가하면 schemaStore에서 제공해주는 정보로 tsconfig.json 파일이 무엇을 의미하는지

어떤 키와 어떤 값이 들어갈 수 있는지 알려주는 도구다.

$schema와 올바른 값이 선언되어 있다면 VS Code나 웹스톰 같은 IDE에서 자동 완성이 가능해진다.

// tsconfig.json
{
  "$schema": "<https://json.shemastore.org/tsconfig.json>",
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "baseUrl": "src",
    "paths": {
      "#pages/*":["pages/*"],
      "#hooks/*": ["hooks/*"],
      "#types/*": ["types/*"],
      "#components/*": ["components/*"],
      "#utils/*": ["utils/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

  • compilerOptions: 타입스크립트를 자바스크립트로 컴파일 할 때 사용하는 옵션
    • target : 타입스크립트가 변환을 목표로 하는 언어의 버전을 의미, 폴리필은 지원 X
    • lib : target 항목에서 지정한 기본 값 외에 추가로 라이브러리 인젝션이 필요한 경우
    • allowJs : 타입스크립트가 자바스크립트 파일 또한 컴파일 할 지 여부
    • skipLibCheck : 라이브러리에서 제공하는 d.ts에 대한 검사 여부 결정, 전체 프로젝트 컴파일 시간이 길어지기 때문에 일반적으로 꺼놓는 경우가 많다.
    • strict : 타입스크립트 컴파일러의 엄격 모드를 제어, 이 모드가 켜지면 아래 옵션들도 true로 설정된다.
      • alwaysStrict
      • strictNullChecks
      • strictBindCallApply
      • strictFunctionTypes
      • strictPropertyInitialization
      • noImplicitAny
      • noImplicitThis
      • useUnknownInCatchVariables
    • forceConsistentCasingInFileNames : 파일 이름의 대소문자를 구분하도록 강제한다.
    • noEmit : 컴파일을 하지 않고 타입 체크만 한다. Next.js는 swc가 타입스크립트 파일을 컴파일 하므로 타입스크립트는 단순히 타입 검사만 할 수 있다.
    • esModulesInterop : CommonJS 방식으로 보낸 모듈을 ES 모듈 방식의 import로 가져올 수 있게 해준다.
    • module : 모듈 시스템을 설정한다. 대표적으로는 commonjs와 esnext가 있다.
    • moduleResolution : 모듈을 해석하는 방식을 설정한다. node는 node_modules 기준으로 모듈을 해석하고, classic은 tsconfig.json이 있는 디렉터리 기준으로 모듈을 해석한다.
    • resolveJsonModule : JSON 파일을 import 할 수 있게 해준다.
    • isolatedModules : 파일에 import나 export가 없다면 단순 스크립트 파일로 인식해 이런 파일이 생성되지 않도록 막는다.
    • jsx : tsx 파일 내부에 있는 JSX를 어떻게 컴파일 할 지 설정한다.
    • incremental : 이 옵션이 활성화되면 타입스크립트는 마지막 컴파일 정보를 .tsbuildInfo 파일 형태로 만들어 디스크에 저장한다. 컴파일러 재호출 시 저장된 정보를 활용해 비용이 적게 드는 방식으로 컴파일을 수행하여 컴파일 속도 향상 효과가 있다.
    • baseUrl : 모듈을 찾을 때 기준이 되는 디렉터리를 지정한다.
    • paths : 상대 경로에 대한 별칭을 지정한다. 보통 #이나 $같은 접두사와 함께 별칭을 지정한다.
    • include : 타입스크립트 컴파일 대상에서 포함시킬 파일 목록을 의미한다.
    • exclude : 타입스크립트 컴파일 대상에서 제외시킬 파일 목록을 의미한다.

3. next.config.js 작성하기

이제 Next.js 설정을 위한 next.config.js를 만들어 보자.

/** @type {import ('next').NextConfig} */
const nextConfig = {
    reactStrictMode: true,
    poweredByHeader: false,
    eslint: {
        ignoreDuringBuilds: true
    }
}

module.exports = nextConfig
  • reactStrictMode : 리액트의 엄격모드 활성화 여부
  • poweredByHeader : 일반적으로 보안 취약점으로 취급되는 X-Powered-By 헤더 제거 여부
  • eslint.ignoreDuringBuilds : 빌드 시 ESLint를 무시할지 여부

next.config.js가 제공하는 설정 파일은 버전별로 조금씩 다르다.

next.js 깃허브 저장소에서 사용중인 버전의 태그를 찾아 들어가면 해당 버전에서 사용 가능한 옵션 확인이 가능하다.

4. ESLint와 Prettier 설정하기

다음으로 정적 분석을 도와주는 ESLint와 Prettier를 설정한다.

앞서 설치한 eslint-config-next는 단순히 코드에 있을 잠재적인 문제를 확인 할 뿐,

띄어쓰기나 줄바꿈과 같은 코드 스타일링은 정의해주지 않기 때문에 @titicaca/eslint-config-triple을 설치해 사용한다.

npm i @titicaca/eslint-config-triple --save -dev

@titicaca/eslint-config-triple에 대한 설정은 깃허브 저장소를 참고하자.

triple-config-kit/packages/eslint-config-triple at main · titicacadev/triple-config-kit

const path = require('path')

const createConfig = require('@titicaca/eslint-config-triple')

const {extends:extendConfigs, overrides} = createConfig({
    type: 'frontend',
    project: path.resolve(__dirname, './tsconfig.json')
})

module.exports = {
    extends: [...extendConfigs, 'next/core-web-vitals'],
    overrides
}

eslint-config-next와 eslint-config-triple이 함께 작동하려면 다음과 같은 별도의 설정이 필요하다.

5. 스타일 설정하기

다음으로는 애플리케이션에 스타일을 적용하기 위해 styled-components를 설치하자.

npm i styled-components

swc에 styled-components 사용한다는 것을 알리기 위해 next.config.js에 다음 구문을 추가한다.

// 📂next.config.js

const nextConfig = {
    reactStrictMode: true,
    poweredByHeader: false,
    eslint: {
        ignoreDuringBuilds: true
    },
    styledComponents: true // 추가된 구문 (swc가 styled-components 사용하는 코드를 더 빠르게 변환시킴)
}

또한, pages/_document.tsx의 Head에 ServerStyleSheet을 추가한다.

import Document, {DocumentContext, DocumentInitialProps, Head, Html, Main, NextScript} from "next/document";
import {ServerStyleSheet} from "styled-components";

export default function MyDocument() {
    return (
        <Html>
            <Head/>
            <body>
            <Main/>
            <NextScript/>
            </body>
        </Html>
    )
}

MyDocument.getInitialProps = async (
    ctx: DocumentContext,
): Promise<DocumentInitialProps> => {
    const sheet = new ServerStyleSheet()
    const originalRenderPage = ctx.renderPage

    try {
        ctx.renderPage = () => originalRenderPage({
            enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props}/>)
        })

        const initialProps = await Document.getInitialProps(ctx)

        return {
            ...initialProps,
            styles: (
                <>
                    {initialProps.styles}
                    {sheet.getStyleElement()}
                </>
            )
        }

    } finally {
        sheet.seal()
    }
}

6. 애플리케이션 코드 작성

프로젝트 구축을 위한 준비를 모두 마쳤으니 이제 웹사이트에 필요한 코드를 작성해보자.

기본적인 폴더 구조는 이미지와 같고 애플리케이션 구동에 필요한 파일은 src 폴더 내부에 있으며, 하위 폴더 목록은 다음과 같다.

- pages: Next.js에서 예약어로 지정해두고 사용하는 폴더로 이 폴더 하위의 내용은 모두 실제 라우터가 된다.
 - /: 메인 페이지
 - /todos/:id: 상세 페이지
- components: 페이지 내부에서 사용하는 컴포넌트를 모아둔 폴더
- hooks: 직접 만든 훅을 모아둔 폴더
- types: 공통으로 사용하는 타입을 모아둔 폴더
- utils: 애플리케이션 전역에서 공용으로 사용하는 유틸성 파일을 모아둔 폴더

 

  • pages: Next.js에서 예약어로 지정해두고 사용하는 폴더로 이 폴더 하위의 내용은 모두 실제 라우터가 된다.
    • /: 메인 페이지
    • /todos/:id: 상세 페이지
  • components: 페이지 내부에서 사용하는 컴포넌트를 모아둔 폴더
  • hooks: 직접 만든 훅을 모아둔 폴더
  • types: 공통으로 사용하는 타입을 모아둔 폴더
  • utils: 애플리케이션 전역에서 공용으로 사용하는 유틸성 파일을 모아둔 폴더

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

마지막으로 Next.js 프로젝트 실행, 빌드, 린트와 관련된 명령어를 package.json에 기재하면 모든 준비가 끝난다.

  "scripts": {
	  "dev": "next dev",
    "start": "next start",
    "build": "next build",
    "lint:es": "eslint '**/*.{js,ts,tsx}'",
    "lint:es:fix": "npm run lint:es -- --fix",
    "prettier": "prettier '**/*' --check",
    "prettier:fix": "prettier '**/*' --write"
  },

프로젝트를 새로 만들 때마다 이렇게 똑같은 설정을 매번 반복하는 것은 비효율적이기 때문에 다음과 같은 방법을 고려해 볼 수 있다.

  1. 보일러 플레이트 프로젝트 생성 후 ‘Template repository’ 옵션 체크하기

이렇게 템플릿 저장소로 만들어두면 다른 저장소 생성 시 이 내용을 모두 복사해서 생성 할 수 있다.

위와 같이 템플릿 저장소로 저장소를 생성하면 어떤 템플릿에서 만들어진 저장소인지 확인할 수 있다.

또 다른 방법은 나만의 create-***-app을 cli 패키지로 만드는 방법이 있다.

깃허브 100% 활용하기

깃허브는 코드 저장소의 역할을 기반으로 CI/CD와 같은 자동화, 보안 이슈 점검, 프로젝트 관리 등 웹서비스 관리 및 운영에 필요한 많은 서비스를 제공한다. 이러한 서비스를 활용해 프론트엔드 개발에 어떤 도움을 얻을 수 있는지 알아보자.

깃허브 액션으로 CI 환경 구축하기

CI (Continuous Intergration)

  • 코드 중앙 저장소에서 여러 기여자의 코드를 지속적으로 빌드하고 테스트해 코드의 정합성을 확인하는 과정이다.
  • CI의 핵심은 저장소에 코드의 변화가 있을때마다 전체 소프트웨어의 정합성 확인을 위한 작업을 자동으로 실행해야 한다는 것이다.
  • 자동으로 실행해야 하는 작업 ? 테스트, 빌드, 정적 분석, 보안 취약점 분석

CI 환경 구축을 위해 자주 쓰인 솔루션 - 젠킨스(Jenkins)

  • 장점 : 편리하고 많은 플러그인을 통해 다양한 기능 통합 가능하다.
  • 단점 : 설치 및 유지보수가 번거롭다.

젠킨스의 대안책 - 깃허브 액션

  • 깃허브를 둘러싼 다양한 이벤트를 기반으로 깃허브에서 제공하는 가상 환경에서 사용자가 원하는 작업을 수행하도록 도와주는 서비스 ⇒ CI/CD 솔루션 대체 가능!
  • 젠킨스를 즉시 대체할 수단은 아니지만 저장소에 있는 코드만으로 CI에 필요한 대부분의 기능을 손쉽게 구현 가능해 빠르고 다양한 CI 환경을 구축해야 하는 상황에 경제적이다.

깃허브 액션의 기본 개념

  • 러너(runner) : 파일로 작성된 깃허브 액션이 실행되는 서버를 의미
  • 액션(action) : 러너에서 실행되는 하나의 작업 단위를 의미
  • 이벤트(event) : 깃허브 액션의 실행을 일으키는 이벤트를 의미
    • pull_request : PR과 관련된 이벤트
    • issues : 이슈와 관련된 이벤트
    • push : 커밋이나 태그가 푸시 될 때 발생하는 이벤트
    • schedule : 저장소에서 발생하는 이벤트와 별개로 특정 시간에 실행되는 이벤트
  • 잡(jobs) : 하나의 러너에서 실행되는 여러 스텝의 모음, 특별히 선언된게 없으면 각각의 잡은 병렬로 실행
  • 스텝(steps) : 잡 내부에서 일하는 하나하나의 작업, 스텝은 병렬로 일어나지 않음.

⇒ 요약 : 스텝들을 엮어서 잡을 만든다. 이때 여러개의 잡은 병렬로 실행되며, 이 잡을 하나 이상 모아둔 것을 액션이라고 한다. 그리고 이 액션을 실행하는 것이 러너이다.

 

Next.js 애플리케이션을 빌드하는 CI 액션 예제

name: chapter7 build
run-name: ${{ github.actor }} has been added new commit.

on: 
	push:
		branches-ignore:
			- 'main'
		
jobs:
	build:
			runs-on: ubuntu-latest
			steps:
				- uses: actions/checkout@v3
				- uses: actions/setup-node@v3
				  with:
					node-version: 16
				- name: 'install dependencies'
				  working-directory: ./chapter7/my-app
				  run: npm ci
				- name: 'build'
				  working-directory: ./chapter7/my-app
				  run: npm run build

 

  • name : 액션의 이름
  • run-name : 액션이 실행될 때 구별 할 수 있는 타이틀 명
  • *on : 언제 이 액션을 실행할지 정의
  • *jobs : 해당 액션에서 수행할 잡을 의미, 한개 이상 설정 가능
    • jobs.build: 예약어가 아닌 임의로 지정한 이름으로 name과 같은 역할
    • jobs.build.runs-on : 어느 환경에서 해당 작업이 실행될지 결정, 별도의 러너 설정 없이 깃허브에서 제공하는 서버를 쓰고싶다면 ubuntu-latest를 사용
    • jobs.build.steps : 해당 잡에서 순차적으로 실행할 작업을 정의
      • uses : 어떤 액션을 사용해 작업할지를 정의
        • actions/checkout@v3 : 깃허브에서 제공하는 기본 액션, 해당 브랜치의 마지막 커밋을 기준으로 체크아웃
        • actions/setup-node@v3 : 깃허브에서 제공하는 기본 액션, 러너에 node.js를 설치, with.node-version 16은 16버전을 설치하겠다는 것을 의미
    • name : ‘install dependencies’ : 해당 스텝의 명칭을 지정, 의존성 설치 작업 수행하기 때문에 'install dependencies'로 지정, run을 통해 수행할 작업을 명시 여기서는 의존성 설치를 위해 npm ci를 선언
    • name : ‘build’ : CI를 위한 작업으로 git checkout, Node.js 설치, 의존성 설치까지 완료 했으니 마무리로 npm run build를 사용해 빌드 수행

위처럼 액션을 작성하면 Next.js 프로젝트를 빌드하는 CI를 작성할 수 있다.

 

직접 작성하지 않고 유용한 액션과 깃허브 앱 가져다 쓰기

깃허브 액션은 Marketplaces라는 서비스를 제공해 사용자가 만들어 놓은 액션을 손쉽게 가져다 쓸 수 있도록 운영하고 있다.

 

calibreapp/image-actions

  • 저장소에 포함되어 있는 이미지를 최적화하는 액션
  • PR로 올라온 이미지를 sharp 패키지를 이용해 거의 무손실로 압축해서 다시 커밋해준다.

lirantal/is-website-vulnerable

  • 웹사이트를 방문해 해당 웹사이트에 라이브러리 취약점이 존재하는지 확인하는 액션
  • Snyk라는 솔루션을 기반으로 작동
  • npm 패키지 실행 도구인 npx로도 실행이 가능 → npx is-website-vulnerable [website 주소]
  • 이상이 없으면 액션이 실행되고 끝나고 취약점 발견되면 액션이 실패한다.

Lighthouse CI

  • 구글에서 제공하는 액션으로 웹성능 지표인 라이트 하우스를 CI를 기반으로 실행 할 수 있도록 도와주는 도구
  • 프로젝트의 URL을 방문해 라이트하우스 검사를 실행한다.

깃허브 Dependabot으로 보안 취약점 해결하기

깃허브에서 제공하는 강력한 기능 중 하나는 Dependabot으로 의존성에 문제가 있다면 이에 대해 문제를 알려주고

가능하면 해결 할 수 있는 풀 리퀘스트까지 열어준다.

의존성 문제를 해결하려면 먼저 의존성버전에 대해 알아야 한다.

 

버전

  • 버전은 주.부.수로 구성되어 있으며 각각의 정의는 다음과 같다.
    • 기존 버전과 호환되지 않게 API가 바뀌면 “주” 버전 up
    • 기존 버전과 호환되면서 새로운 기능을 추가할 때는 “부” 버전 up
    • 기존 버전과 호환되면서 버그를 수정한 것이라면 “수” 버전 up

버전 관리시 주의할 점

  • 특정 버전으로 패키지 배포하고 나면 그 버전의 내용은 절대 변경해선 안된다. 변경 사항이 있을 경우 무조건 새로운 버전으로 배포한다.
  • 주 버전 0은 초기 개발을 위해 쓴다. 0으로 시작하는 실험 버전 라이브러리는 사용에 주의를 기울여야 한다.
  • 수 버전은 반드시 그 이전 버전 API와 호환되는 버그 수정의 경우에만 올린다. 만약 버그 수정이 API 스펙 변경을 동반한다면 반드시 주 버전을 올려야 한다.

유의적 버전은 어디까지나 개발자들 간의 약속일 뿐, 정말로 해당 API의 버전이 이 유의적 버전에 맞춰 구현되어 있는지는 알 수 없다는 점을 염두에 둬야 한다.

 

의존성

  • dependencies : 해당 프로젝트를 실행하는 데 꼭 필요한 패키지가 여기에 선언된다. npm install 패키지명으로 실행하면 여기에 추가된다.
  • devDependencies : 해당 프로젝트를 실행하는 데는 필요하지 않지만 개발 단계에서 필요한 패키지들을 여기에 선언한다. npm install 패키지명 —save-dev를 실행하면 여기에 추가된다.
  • peerDependencies : 서비스보다 라이브러리와 패키지에서 자주 쓰이는 단위로 직접적으로 패키지를 require, import 하진 않지만 호환성으로 인해 필요한 경우를 의미한다.

의존성 관련 이슈를 방지하는 가장 좋은 방법?

  • 의존성을 최소한으로 유지하는 것
  • 내재화 할 수 있는 모듈은 최대한 내재화
  • 내재화가 불가능하다면 가능한 널리 알려져있고 활발히 유지보수 되는 패키지를 사용

리액트 애플리케이션 배포하기

리액트 앱을 손쉽고 빠르게 배포할 수 있도록 도와주는 SaaS 서비스 3가지

Nextlify

  • 웹 애플리케이션을 배포할 수 있도록 도와주는 클라우드 컴퓨팅 서비스

Vercel

  • Next.js를 비롯한 Turborepo, SWC를 만든 회사
  • Nextlify와 비슷한 클라우드 플랫폼 서비스

DigitalOcean

  • 미국의 클라우드 컴퓨팅, 호스팅 플랫폼 업체
  • Vercel, Netlify와 비슷하게 저장소를 바탕으로 바로 배포할 수 있는 서비스를 제공

리액트 애플리케이션 도커라이즈하기

기존 배포 체계의 문제점

  • 애플리케이션을 자유롭게 커스터마이징하는데에 제약이 있다.
  • 비용 체계가 유연하지 못하다.

위와 같은 문제점으로 인해 애플리케이션을 하나의 컨테이너로 만들어서 빠르게 배포 하는게 일반적이며 이러한 컨테이너를 만드는 데 사용되는 것이 도커

 

도커

  • 개발자가 모던 어플리케이션을 구축, 공유, 실행하는 것을 도와줄 수 있도록 설계된 플랫폼으로 지루한 설정 과정을 대신해주어 코드 작성에만 집중 할 수 있다.
  • 서비스 운영에 필요한 애플리케이션을 격리해 컨테이너로 만드는데 이용하는 소프트웨어
  • 이미지 상태로 앱을 준비해두어 도커이미지를 실행 할 최소한의 환경이 갖춰지면 어디서나 웹앱을 배포할 수 있다.
  • 특정 배포 서비스 (ex. vercel, netlify)에 종속적이지 않아 좀 더 유연하다.

리액트 앱을 도커라이즈하는 방법

도커라이즈

  • 애플리케이션을 도커 이미지로 만드는 과정
  • 즉, 애플리케이션을 신속하게 구축해 배포할 수 있는 상태로 준비하는 것

도커에서 자주 사용되는 용어

  • 이미지 : 컨테이너를 만드는데 사용되는 템플릿이다. 만들기 위해 Dockerfile이 필요하며 이 파일을 빌드하면 이미지를 만들 수 있다.
  • 컨테이너 : 도커의 이미지를 실행 한 상태를 컨테이너라한다. 이미지가 목표하는 운영체제, 파일 시스템, 각종 자원 및 네트워크등이 할당되어 실행될 수 있는 독립된 공간이 생성된다.
  • Dockerfile : 어떤 이미지 파일을 만들지 정의하는 파일이다. ‘도커라이즈한다’라고 할 때 가장 먼저 하는 것이 이 Dockerfile을 만드는 것이다.
  • 태그 : 이미지를 식별 할 수 있는 레이블 값을 의미한다. 이름:태그명 형태로 구성되어 있다. ex) ubuntu:latest 면 unbuntu의 latest인 이미지를 의미
  • 리포지터리 : 이미지를 모아두는 저장소, 이름에 다양한 태그로 지정된 이미지가 모여있는 저장소이다.
  • 레지스트리 : 리포지터리에 접근 할 수 있게 해주는 서비스를 의미한다. 대표적으로는 도커 허브가 있다.
  •  

본격적으로 create-react-app 과 create-next-app 을 도커 이미지로 만들어보자.

 

1. 도커 설치하기

도커 홈페이지에서 도커 데스크탑을 다운 받는다.

 

정상적으로 설치가 완료되면 다음 명령어로 도커의 버전을 확인할 수 있다.

docker --verison

 

2. create-react-app을 위한 Dockerfile 작성하기

프론트엔드 애플리케이션이 도커 이미지에서 해야 할 작업을 간단히 요약하면 다음과 같다.

  • 운영체제 설정
  • Node.js 설치
  • npm ci
  • npm run build
  • 프로젝트 실행

위에서 언급한 모든 과정을 Dockerfile에 기재하면 된다.

프로젝트의 루트에 Dockerfile이라는 이름의 파일을 생성하고 다음과 같이 작성한다.

# 어떤 베이스 이미지에서 실행될지 결정
# Node.js 18.12.0 버전이 설치되어 있는 이미지를 의미하며 alpine 3.16버전 의 운영체제 위에서 실행되는 이미지라는 것을 의미
# as build : 이 베이스 이미지를 build 단계에서만 쓰겠다는 것을 의미
FROM node:18.12.0-alpine3.16 as build

# 작업을 수행하고자 하는 기본 디렉터리를 의미
WORKDIR /app

# COPY는 파일을 복사하는 명령어
COPY package.json ./package.json
COPY package-lock.json ./package-lock.json

# 의존성 설치 명령어
RUN npm ci

# 의존성 설치 후 빌드를 위해 모든 리소스를 복사
COPY . ./

# 애플리케이션 빌드
RUN npm run build

여기까지 작성 후 도커 이미지를 빌드하기 위해 다음과 같이 터미널에 입력한다.

docker build . -t cra:test

그럼 다음과 같이 빌드가 진행된다.

 

도커 데스크탑에서 이미지가 생성된 것을 확인 할 수 있다.

그럼 이 이미지를 실행해보자.

 

그럼 18.12.0 버전의 Node.js 만 실행된 것을 확인 할 수 있다.

그 이유는 Node:18.12.0-Alpine3.16을 만든 Dockerfile을 살펴보면 답을 알 수 있다.

 

마지막에 CMD [”node”]로 인해 Node.js가 실행된 것이다.

우리가 원하는 것은 애플리케이션이 실행되는 것이므로 다음과 같은 작업이 추가되어야 한다.

  • 빌드된 웹앱을 NGINX가 서비스 할 수 있도록 설정
  • 이미지 실행했을 때 해당 웹페이지에 접근 할 수 있어야 함
  • 웹페이지 접근에 필요한 빌드 파일만 남겨두고 용량 최소화

위 목표 달성을 위해 Dockerfile에 다음 내용을 추가한다.

# 빌드된 정적 파일 서비스를 위해 최신 버전의 NGINX가 설치된 알파인 리눅스 설치
FROM nginx:1.23.2-alpine as start

# 빌드한 파일을 NGINX가 서비스 할 수 있도록 설정 파일 복사
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf

# --from=build는 앞서 FROM... as build로 선언한 단계를 의미
# build라는 단계에서 복사해 오는데 /app/build만 가져와 현재의 단계인 start의 원하는 위치인 /usr/share/nginx/html에 복사
# build 단계에 필요한 리소스만 가져와 start 단계에서 사용할 수 있게 됨
COPY --from=build /app/build /usr/share/nginx/html

# 나중에 도커이미지를 실행 할 때 호스트 운영체제에서 오픈되는 포트
# 이미지를 만드는 사람이 해당 이미지를 컨테이너로 실행할 때 어떤 포트가 열려있는지 알려주는 용도
EXPOSE 3000

# 컨테이너가 시작됐을 때 어떤 명령어를 시작할지 결정
# Dockerfile 내부에서 단 한번만 실행할 수 있으며, 여기서는 NGINX의 데몬을 시작하도록 만듦.
ENTRYPOINT ["nginx", "-g", "daemon off;"]

 

해당 코드를 추가하고 다시 빌드를 실행한다.

이미지가 이전에 비해 훨씬 작아진 것을 확인 가능하다.

EXPOSE에서 명시했던 3000포트를 열어서 이미지를 실행한 후 http://localhost:3000/ 접근해보면

creat-react-app으로 빌드한 애플리케이션이 NGINX를 통해 서비스되는 것을 확인할 수 있다.

 

대부분의 기업에서는 애플리케이션을 도커라이즈해서 배포하고, 각 이미지를 관리하고 보관하면서

배포 관련 히스토리를 남겨두거나 빠르게 롤백하는 등의 용도로 도커를 사용하고 있기 때문에

프론트엔드 개발자라면 적어도 자신이 만든 애플리케이션을 도커라이즈하는 방법까지 숙지하고 있어야 한다.