[#8] React Front 및 Redis 작업하기

Written by 코드팩토리 JC

1월 15, 2024

코드팩토리 Docker 부터 Kubernetes 까지

이번 글에서는 React Front 와 Redis 작업을 하여 Docker Compose 에 대한 작업을 마무리 해보도록 하겠습니다.

빠른 개발과 편의성을 위해 create-react-app 을 사용하도록 하겠습니다. create-react-app 에대한 정보는 아래 링크를 참조해주시면 되겠습니다.

Create React App

프로젝트 루트 디렉터리에 터미널을 실행해주시고 npm init react-app reactfront 를 실행해주세요. 커맨드 실행이 완료되면 저희 폴더 구조에 맞게 reactfront 라고 되어있는 폴더를 ReactFront 로 대문자화 해주시면 되겠습니다.

커맨드가 완료되면 여러 폴더 및 파일들이 생성될텐데 각 폴더 및 파일에대한 설명은 이 강의의 범위를 벗어나기 때문에 생략하도록 하겠습니다. React에대해 궁금하신점이 있으시면 아래 링크를 참조해주세요.

React 홈페이지

가장먼저 ReactFront 폴더에 Dockerfile 을 생성하고 아래 코드를 붙여넣겠습니다.

FROM node:12-alpine

# work directory
WORKDIR /usr/app

# Copy dependencies first for effective caching
COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 6000

CMD ["npm", "run", "start"]
Dockerfile

지금까지 써왔던 다른 Docker 스크립트와 별 차이 없이 컨테이너를 빌드하고 npm run start 라는 커맨드를 실행시키는 스크립트입니다.

create-react-app 을 실행하며 생성된 package.json 을 잠시 보면 npm run start 커맨드에 react-scripts start 라는 커맨드가 매핑되어있는걸 보실 수 있습니다. 도커 컨테이너에서 ReactFront 컨테이너를 실행할때 해당 커맨드를 사용하게 될것입니다.

다음은 이전 강의에서 작업했던 Nginx 폴더의 dev.conf 파일을 고쳐보도록 하겠습니다.

# Express Server에 대한 정의를 해줍니다.
# Docker Compose를 사용하면 자동으로 Docker Compose에서 사용한
# 이름에 따라 도커 네트워크에서 DNS가 생성됩니다.
# 저희 Express Server는 express_server라는 이름으로
# Docker Compose 파일에 정의해놨기 때문에 해당 이름을
# DNS로 사용하시면 요청이 Docker Compose 내부 내트워크를 타고
# Express Server로 전달이 됩니다.
# 포트가 4000번이 아니고 3000번인 이유는
# 호스트를 통해서 네트워크 연결이 될 경우 4000번이 맞지만
# 다이렉트하게 Express Server의 컨테이너에 꼿히기때문에
# 내부 포트인 3000번을 사용해주셔야 합니다.
upstream express_server {
    server express_server:3000;
}

upstream react_front {
    server react_front:3000;
}

server {
    listen 80;

    # 로그파일을 저장하는 부분입니다. 이 부분을
    # Docker Compose에서 Volume 쉐어를 해놓으면
    # 호스트 기기에서도 도커에서 생성된 로그파일을 볼수있습니다.
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # React Front 컨테이너로 루트 패스를 모두
    # 매핑해줍니다.
    location / {
        proxy_pass http://react_front;
    }

    # 이건 딱히 신경쓰실 필요는 없는데
    # React 에서 파일이 변경 됐을때
    # 브라우저로 refresh를 하라고 알려주는 패스를
    # 매핑해주는 역할을 합니다.
    location /sockjs-node {
        proxy_pass http://react_front;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
    }

    # /api의 경로로 Nginx를 hit 하게되면
    # express Server로 reverse proxy 역할을 하라는 코드입니다.
    # 다만 서버 개발의 편의를 위해 /api 부분을 제외하고
    # /api 이후에 오는 부분들만 전달하게됩니다.
    # rewrite 룰을 정의하지 않을경우
    # Express Server에서 모든 api포인트에 /api를 최상위
    # 라우트로 지정해줘야해서 귀찮아집니다.
    location /api {
        proxy_pass http://express_server;
        rewrite /api/(.*) /$1 break;
    }
}
Nginx

위 스크립트로 dev.conf 파일을 통째로 덮어쓰기 해주세요. 추가된부분은 location / 부분과 location /socketjs-node 입니다. 변경 내용은 주석을 참조해주세요.

마지막으로 ReactFront 의 docker-compose.yml 파일 및 .env파일 작업을 해줄 차례입니다. 아래 코드를 docker-compose.yml 파일에 추가해주세요.

react_front:
  build: ReactFront
  ports:
    - "${REACT_FRONT_PORT}:3000"
  volumes:
    - /usr/app/node_modules
    - ./ReactFront:/usr/app
  env_file:
    - .env
YAML

아래 코드는 .env 파일에 추가해주세요.

## React Front
REACT_FRONT_PORT=6000
ShellScript

이건 약간의 본론에서 벗어나기는 하지만 Nginx 를 reverse proxy로 사용하게된 이상 ReactFront, ExpressServer, PollingServer, MongoDB 는 모두 더이상 포트를 호스트 디바이스와 매핑해주실 필요가 전혀 없습니다. 어차피 Nginx 의 8080 포트를 타고들어가서 Docker Compose 내부 네트워크로 통신이 이루어지기 때문이죠.

docker-compose up –build 커맨드를 프로젝트 루트에서 실행하면 ReactFront 를 포함한 5개의 컨테이너가 실행됩니다.

이쯤되면 5개의 컨테이너를 빌드하는데 컴퓨터에 따라 시간이 좀 오래걸릴 수 있습니다. 현재는 한개의 쓰레드만 사용해서 컨테이너를 순서대로 빌드를 하지만 docker-compose build –parallel 을 실행하시면 모든 컨테이너를 병렬로 빌드 하실 수 있습니다. 빌드가 완료되면 docker-compose up (빌드 태그를 생략하고)를 실행해서 병렬로 빌드된 컨테이너들을 한번에 실행하실 수 있습니다.

잘 따라오셨다면 브라우저에서http://localhost:8080 을 접속하셨을때 아래와같은 페이지를 볼 수 있으십니다. (create react app의 버전에 따라 랜딩 페이지는 살짝 다를수도 있습니다 — 400만 안뜨시면 돼요).

CRA

드디어 Docker Compose 로 프론트엔드를 띄웠다는 만족감을 즐기며 잠시 랜딩페이지를 감상하시다 ReactFront 폴더로 돌아오셔서 npm install axios 커맨드를 실행해주세요. ReactFront 에서 HTTP 요청을 할때 사용하게될 라이브러리입니다.

ReactFront/src 폴더에 있는 App.js 의 코드를 아래 코드로 대체해주세요.

import React, {useEffect, useState} from 'react';
import logo from './logo.svg';
import './App.css';
import axios from 'axios';

function App() {
    const [posts, updatePosts] = useState([]);

    useEffect(() => {
        async function fetchData() {
            const resp = await axios.get('/api/posts');

            updatePosts(resp.data.data)
        }

        fetchData();
    }, []);

    return (
        <div className="App">
            {posts.map((x) => {
                return (
                    <div style={{
                        border: '1px solid #000000',
                        margin: 20,
                        padding: 10,
                    }}>
                        <a href={x.url}>
                            <div>
                                id: {x._id}
                            </div>
                            <div>
                                title: {x.title}
                            </div>
                            <div>
                                url: {x.url}
                            </div>
                        </a>
                    </div>
                )
            })}
        </div>
    )
        ;
}

export default App;
JSX

GET /api/posts 로 요청을 해 응답 값들을 텍스트로 매핑해주는 역할을 합니다. axios 를 설치했기때문에 ReactFront 컨테이너를 다시 빌드해야합니다. docker-compose up –build 를 이용해 다시 빌드 후 실행해주시면 아래와 비슷한 화면이 http://localhost:8080 에 뜹니다.

react result

각 박스를 클릭하면 해당되는 Hacker News로 이동하실 수 있습니다.

마지막으로 Redis 컨테이너를 작업하고 마무리하겠습니다.

Redis 는 조금 독특하게 따로 폴더를 생성하지 않고 Docker Compose 에서 실행하는 방법을 알려드리겠습니다. 아래 코드를 docker-compose.yml 에 추가해주세요.

redis:
  image: redis:5
YAML

위와같이 image 키에 직접 이미지를 명시해주면 Dockerfile 없이도 컨테이너를 빌드하고 실행할 수 있습니다.

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
const mongoose = require('mongoose');
const axios = require('axios');
const Post = require('mongoose/Post');
const redis = require('redis');
const {promisify} = require('util');

const mongoUri = 'mongodb://root:root@mongodb:27017/admin';

const client = redis.createClient({
    host:'redis'
});
const getAsync = promisify(client.get).bind(client);
const setAsync = promisify(client.set).bind(client);
const incrAsync = promisify(client.incr).bind(client);

mongoose.connect(mongoUri, {useNewUrlParser: true});

app.get('/count', async (req, res) => {
    const count = await getAsync('count');

    console.log(count);

    res.json({
        count
    })
});

app.get('/posts', async (req, res) => {
    console.log('Getting Posts Started!!!');

    /*
    * 만일 count 라는 키가 존재하지 않으면
    * count 를 1로 세팅하고
    * count가 존재하면 +1을 합니다.
    * */
    const count = await getAsync('count');

    if(count !== 0 && !count){
        await setAsync('count', 1);
    }else{
        await incrAsync('count');
    }

    let posts = await Post.find();

    if (posts.length === 0) {
        await axios.get('http://polling_server:3000/');

        posts = await Post.find();
    }

    console.log('Getting Posts Finished');

    res.json({
        success: true,
        version:1,
        data:posts
    });

});

app.listen(port, () => {
    console.log(`server is listening at localhost:${port}`);
});
JavaScript

위 코드로 ExpressServer 의 server.js 코드를 덮어씌워주세요. GET /count 라우트가 추가되고 GET /posts 라우트에 요청을 할때마다 count 변수를 +1 해주는 코드를 추가하였습니다.

이제 아래 코드를 ReactFront/src/App.js 파일에 덮어씌우겠습니다.

import React, {useEffect, useState} from 'react';
import logo from './logo.svg';
import './App.css';
import axios from 'axios';

function App() {
    const [posts, updatePosts] = useState([]);
    const [count, updateCount] = useState(null);

    useEffect(() => {
        async function fetchData() {
            const resp = await axios.get('/api/posts');
            const respC = await axios.get('/api/count');

            updatePosts(resp.data.data);
            updateCount(respC.data.count);
        }

        fetchData();
    }, []);

    return (
        <div className="App">
            <div>
                count: {count}
            </div>
            {posts.map((x) => {
                return (
                    <div style={{
                        border: '1px solid #000000',
                        margin: 20,
                        padding: 10,
                    }}>
                        <a href={x.url}>
                            <div>
                                id: {x._id}
                            </div>
                            <div>
                                title: {x.title}
                            </div>
                            <div>
                                url: {x.url}
                            </div>
                        </a>
                    </div>
                )
            })}
        </div>
    )
        ;
}

export default App;
JSX

Count 를 서버에서 가져오고 HTML 로 display 해주는 코드입니다. 변경후 docker-compose up –build 를 재실행하시고 http://localhost:8080 에 접속하시면 위쪽에 count 가 뜨고 새로고침을 할때마다 count 가 +1 되는걸 보실 수 있습니다.

여기까지 따라오시는데 고생하셨습니다. Docker Compose 강의는 여기서 마치고 조만간 이 컨테이너들을 Kubernetes 에 올리는 강의로 다시 찾아뵙겠습니다. 감사합니다.GitHub 링크 바로가기

관련 포스트

플러터에서의 Immutable Programming: copyWith 함수 마스터하기!

플러터에서의 Immutable Programming: copyWith 함수 마스터하기!

서론 불변 프로그래밍: 현대 개발의 핵심 현대 소프트웨어 개발에서 불변 프로그래밍(Immutable Programming)의 중요성은 간과할 수 없는 요소입니다. 플러터(Flutter)에서도 마찬가지로 불변 프로그래밍 개념이 매우 중요하며, copyWith 함수는 이러한 불변성을 유지하는 데 핵심적인 역할을 합니다. 이 글에서는 플러터를 배우기 시작하는 개발자들에게 불변 프로그래밍의 중요성을 강조하고, copyWith 함수의 역할과 사용 방법에 대해 설명 해보겠습니다!...

ChatGPT가 이야기하는 2024년 개발자 로드맵

ChatGPT가 이야기하는 2024년 개발자 로드맵

서론 개발자의 여정을 시작하며 안녕하세요, 미래의 개발자 여러분! 오늘부터 시작하는 여러분의 개발 여정에 함께할 수 있어서 기쁩니다. 2023년은 기술이 매우 빠르게 변화하는 해였으며, 이러한 변화 속에서 개발자가 되기 위한 길은 더욱 다채롭고 흥미로워졌습니다. 이 로드맵은 초보자인 여러분이 개발의 세계에 첫발을 내딛는 데 필요한 기초부터 시작해, 점차 심화 단계로 나아가는 길을 안내해 드릴 것입니다. 백엔드 개발 이 글은 단순히 기술을 배우는 것 이상의 의미를 가집니다....

Flutter Freezed 플러그인! Entity Code Generation은 이거 하나로 끝!

Flutter Freezed 플러그인! Entity Code Generation은 이거 하나로 끝!

https://youtu.be/i5p6wXLAX7I 서론 Flutter 는 Code Generation 기능이 상당히 많이 활성화되어 있어요. 흔히들 많이 사용하는 json_serializable 라이브러리도 있고 retrofit 및 chopper 라이브러리도 있습니다. 오늘 알려드릴 freezed 또한 데이터 클래스에 편의 기능들을 제공해주는 code generation 라이브러리입니다. Freezed vs Json Serializable Code Generation 이라는...