[애플리케이션] React + Django + Nginx를 EC2에 배포! 거기에 Docker를 곁들인.. (2)
시작
이번에 작성할 글은 지난 게시물에 이어서.. 작성하겠습니다.
저번 게시물은 장고를 대부분 API로만 사용을 했기 때문에, API와 관련된 장고 사용법만 공부를 했다.
오늘은 리액트 부분만 간단하게 정리하고, 글을 쓰다 생각나는 애들은 뭐 추가해 보도록 하겠다.
학습내용
리액트는 자바스크립트 라이브러리로 SPA(Singl Page Application)나 모바일 애플리케이션에 사용이 되는데,
이 애플리케이션 배포시에는 SPA로 구성하여 배포했다.
리액트를 학습하면서 컴포넌트라는 단어를 처음 듣게 되었는데, 여기서 컴포넌트는 UI를 구성하고 있는 여러 조각들을 컴포넌트라고 부른다.
import React from "react";
function Main({ name }) {
const username = name;
return (
<div>
<h1>안녕하세요. {username}</h1>
</div>
);
}
export default Main;
위의 예제 형태로 어떤 표현하고자하는 UI를 만들게 될 때, 그것을 하나의 컴포넌트로 부른다.
이 과정을 진행하면서 생각보다 한 페이지를 구성하는데 드는 컴포넌트의 수가 많이 필요한 걸 깨달았다.
회원가입, 로그인, 정보확인 등등등...
리액트로는 이런 형태의 페이지들 + 장고에서 만든 API랑 연동된 페이지들을 구성했다.
import React, { useState, useEffect } from "react";
function Main({ username }) {
const [userInfo, setUserInfo] = useState({
name: '',
registerdate: '',
emailaddress: ''
});
const GetUserInfo = async (username) => {
try {
const response = await fetch(`/api/get-user-info/${username}`);
const response_data = await response.json();
return response_data;
} catch (e) {
console.error("API 확인 요망", e);
return null;
}
};
useEffect(() => {
const fetchUserData = async () => {
const data = await GetUserInfo(username);
if (data) {
setUserInfo({
name: data.name,
registerdate: data.registerdate,
emailaddress: data.emailaddress
});
}
};
fetchUserData();
}, [username]);
return (
<div>
<h1>안녕하세요. {userInfo.name}님</h1>
<h2>가입일은 {userInfo.registerdate} 입니다.</h2>
<h2>이메일 주소는 {userInfo.emailaddress} 입니다.</h2>
</div>
);
}
export default Main;
예시로 만든 API 불러오는 컴포넌트를 하나 만들었다.
우선, usestate를 이용해서 상태관리 초기화를 진행해주는 세팅을 하고,
fetch를 이용해서 api를 연결하고 데이터를 반환하며,
useeffect를 username이 바뀌거나, 컴포넌트 마운트 될 때, 다시 데이터를 비동기로 가져오도록 설정을 하고
유저 이름, 가입날짜, 이메일 주소를 보여주는 형태의 컴포넌트이다.
이런 컴포넌트를 App.jsx에 import 하고 이런 정보를 상태 관리를 통해서 필요한 컴포넌트로 넘겨줄 수 있다.
주로 OCR 데이터의 수정 부분에서 이런 형태를 사용했으며, 이런 방식을 클라이언트사이드 렌더링 형식(?)으로 볼 수 있다.
다른 로그인 유지, 토큰 세션 관리 등은 서버사이드 렌더링 방식으로 구현되어 있다.
간단하게, 서버사이드 렌더링이랑 클라이언트사이드 렌더링은 다음과 같다.
서버사이드 렌더링 (SSR)
서버에서 렌더링을 해 클라이언트에게 전달하는 방식, 초기 로딩 속도는 빠르고 SEO에 유리
그러나 이용자 많으면 서버 부하(모든 요청에 서버가 페이지 렌더링해서)
클라이언트사이드 렌더링 (CSR)
서버는 데이터 제공 클라이언트에서 렌더링 하는 방식, 초기 로딩 속도는 느리고 SEO에 불리
그러나 동적인 사용자 경험 제공 가능(페이지가 동적 렌더링)
하지만,, 앱을 다 구성하고 나서 이렇게 렌더링 하는 방식이 따로 존재한다는 걸 알았으며,
완성한 앱은 저 두 방식을 혼합해서 만들었다는 것이다.
이외에 리액트를 학습한 것들은 없던 것 같다.
다만, 빌드를 위한 라이브러리?? 그것에 대해서 바꿨다.
기존에 CRA라 불리는 저런 형태로 사용을 했었는데, 지원 종료? 가 떠서 VITE라는 빌드 라이브러리로 바꿨다.
npx creat-react-app front
아래는 VITE로 앱 만들 때
npm create vite@latest
우선 빌드 라이브러리 바꾸면서 기억에 남는 것은 빌드속도가 어마어마하게 빨랐다는 거??
CRA 쓸 때는 몇 초 넘게 걸렸는데, VITE 빌드는 ms급으로 나왔다..
그래서 API랑 연결해서 테스트할 때, 확인하기 편했다.
문제 해결
1번
최대한 서버사이드 형태를 유지하면서 구성하였고, OCR 데이터 수정 부분이나 DB로 전송 부분에서는 클라이언트사이드 형태로 동적 UI를 제공하는 방향으로 구성했다.
장고와 리액트로 웹을 구성하고 난 뒤, 성능 향상이 얼마나 있었는지 간단한 웹 컴포넌트 로딩 테스트를 진행했다.
Streamlit으로 구성된 웹은 평균 1.4초 대의 로딩 시간이,
장고와 리액트로 구성된 웹은 평균 0.2초 대의 로딩 시간이 걸리는 걸 확인했다.
기존의 웹 대비 80%의 성능 향상이었다..(대박)
나름 최대한 성능 최적화를 했다고 생각하지만, 분명 놓친 부분이 있었을 거라고 생각하는데
만드는데 편리함이 주는 것보다 훨씬 얻는 이득이 큰 성능 향상이었다.
2번
원하는 UI를 구성하는 게 너무 편했다.
React 자체의 레퍼런스도 많고, 사용자 친화적인 UI 형태도 많이 제공되어서 전체적인 프레임을 유지하면서 원하는 UI로 변화가 쉬웠다.
3번
주로, usestate나 useefect를 이용한 상태 관리를 통해 OCR 데이터 수정이나 데이터 전송 같은 플로우를 설정하고, 파악하는데 어렵지 않았다.
다만, 조금 복잡하게(실력 부족) 플로우가 구현되어 있어서, 설명 문서나 주석 없이는 이해하기 힘든 부분도 존재한다.
4번
다른 기능을 추가하는 로직이 이전에는 복잡하고(기존 코드를 분석해야 했음), 어려웠다.
기능 추가 로직 중 하나를 예로 들자면,
초기에 로그인을 유지하기 위한 방법으론 LocalStorage, SessionStorage를 이용해서 클라이언트사이드로 구현을 했었다.
다만, 이렇게 구현이 되면 자바스크립트를 이용해서 유저정보가 접근이 가능하고, 수정이 가능하다는 단점이 존재했다.
거기에 암호화가 되지 않아서 정보가 너무 노출되어 있다는 점이 있었다.
(정보 자바스크립트로 수정해 봄 -> 너무 잘 된다..)
뭐,, 큰 서비스가 아니어서 큰 문제가 될 것 같지는 않았지만(안일한 생각), Cognito가 토큰을 제공하고 있다는 점을 알고 토큰을 이용해서 유저 정보 보안을 강화해 보자 했다.
JWT를 이용해서 Cognito에서 가져오는 토큰 정보를 서버 쪽에서 검증하고, 현재 유저정보와 동일한지 쿠키를 이용해 체크확인한다.
여기에 보안상(?) 서버쪽에서 따로 RSA로 전송되는 유저 정보 데이터의 암호화 복호화를 진행했었다.
(이후에 알고 보니 HTTPS 자체로도 데이터가 암호화 복호화가 된다는 점을 확인했다.-> 나도 모르게 2중 보안???)
현재 유저정보가 Cognito에서 가져온 정보와 동일하면 로그인 유지를 하는 방식으로 진행했다.
쿠키를 통해 접속을 유지하는 방식을 채택하면서 유저정보보안을 조금 강화했었던 기억이 난다.(보안!!)
배포
이렇게 만든 웹을 ec2에 배포를 하는데 겪은 문제중 가장 임팩트 있던게 있는데,
바로 장고와 리액트만으로 도커, 도커 컴포즈를 이용해서 배포가 가능하다고 생각했다는 멍청한 생각이었다.
위의 방식으로 배포를 하게 되면, 클라이언트가 리액트 앱을 요청하게 되면 정적 파일 서빙이 안 되는 문제가 있었다.
(즉, 화면이 안보임)
저런 방식으로 배포를 진행하다가 라우팅 및 리버스 프락시 역할을 해주는 웹 서버가 필요하다는 사실을 알았다.
간단하게 말해서, Nginx 없이 하면 클라이언트 요청을 애플리케이션 서버에 전달하고, 그 서버의 응답을 클라이언트로 보내줘야 하는데, 이 부분을 Nginx가 맡아서 해준다는 것이다.
(나는 중요한 녀석을 빼먹은 것..)
정적 파일 보여주는 것도 해주고 로드 밸런서의 역할도 하는데,
정적 파일은 폴더 마운트 해서 처리했고,
로드 밸런서는 단일 서버로 구축되어 있어서 + ACL 이 있어서 사용은 안 했다.(가 맞겠지??)
server {
listen 80;
server_name test-deployment.com;
# React 빌드 파일을 서빙하는 경로
root "리액트 빌드 파일 경로";
# React 앱의 모든 요청을 처리
location / {
try_files $uri /index.html;
}
# Django API 요청 처리
location /api/ {
proxy_pass http://127.0.0.1:8000; # Django가 실행되는 주소 (도커 컨테이너 명으로 넣어도 됨)
proxy_pass http://django_backend:8000; # 이런식으로 작성해도 됨 (단 도커 컴포즈 네트워크가 같으면 됨)
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
이런 식으로 nginx.conf 파일을 설정하고 진행했다.
이렇게 nginx로 앱 요청 처리를 해주니까. 배포가 잘 되던... ㅠ
리액트, 장고 각각 컨테이너 이미지를 만들고, nginx는 최신 nginx를 이용했다.
도커 컨테이너 3개를 이제 같은 도커 네트워크 상에 두고, 이미지를 이용해 배포했다.
services:
django:
image: Django
container_name: django
ports:
- "8000:8000"
expose:
- "8000" # 내부 네트워크에서 접근 가능하도록 expose 추가
networks:
- shared-network
env_file:
- django.env # 환경 변수 파일을 불러오기 위해 추가
environment:
- DJANGO_SETTINGS_MODULE=back.settings
command: " gunicorn hyback.wsgi:application --bind 0.0.0.0:8000"
volumes:
- react-static-files:/usr/share/react-static # Django 컨테이너에 React 정적 파일 마운트
react:
image: React
container_name: react
ports:
- "3030:3000"
expose:
- "3000" # 내부 네트워크에서 접근 가능하도록 expose 추가
volumes:
- react-static-files:/app/build # React 컨테이너의 빌드 경로에 볼륨 마운트
networks:
- shared-network
env_file:
- react.env # 환경 변수 파일을 불러오기 위해 추가
nginx:
image: nginx:latest
container_name: nginx
ports:
- "80:80" # 외부에서 접근할 수 있는 포트
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf # Nginx 설정 파일을 마운트
- react-static-files:/usr/share/nginx/html # React 정적 파일을 Nginx에 마운트
depends_on:
- django
- react
networks:
- shared-network
networks:
shared-network:
external: true
volumes:
react-static-files: # 볼륨 정의 추가
위와 같은 형태로 도커 컴포즈를 이용해서 배포를 진행했으며,
각 앱마다 env 파일을 만들어서 각자 중요한 녀석은 따로 보관하기
리액트의 경우 env 만들 때 앞부분에 REACT_APP_~~~~ 이런 식으로 필히 적어야 인식한다.
뭔가 쓰면서 생각난 거 적기도 하고, (최근에 끝냈는데....) 나만 이해가능하게 글을 쓴 것 같다.
딱히 코드보다는 이거 이거 했다만 쓴거같다.
웹 하나 만드는데 생각보다 오랜 시간이 들고, 컴포넌트, API 등 하나하나 신경 써줘야 하는 게 정말.. 쉽지 않은 작업 같다.
다만, 결과물을 이렇게 직접적으로 체험이 가능하니까 더 뭐랄까 새로운 무언갈 이용해서 만들고 싶다는 생각이 든다.
다른 웹 만들고 있는데, 여기선 웹 소켓 적용예정이라 다음번엔 웹 소켓 적어보겠다.