오늘은 지금까지 배웠던 지식들을 활용하여 Node(Express)와 React를 합쳐보았다.
그리고 리액트 코드에서 ‘fetch’ API를 이용하여 Express에 요청하고 Express에서 mongoDB에 저장된 데이터를 가지고 와 화면에 보여주는 과정을 구현해보았다.
최종적으로 두개의 환경을 합쳤을 때 실제로 배포가 되는것까지 확인하기 위해 Heroku를 통해 배포해 보았다.
먼저 client 디렉토리에는 리액트를, server 디렉토리에는 node + express 환경을 설정해주었다.
그 후, client 에는 CRA(Create-react-app), server에는 express-generator를 이용하여 손쉽게 개발 환경을 구성하였다.
npm install -g create-react-app
create-react-app client
npm install -g express-generator
express server
그리고 client 폴더에 있는 리액트 코드를 빌드하여 빌드된 파일을 server 폴더에 위치시켜줄 것이다.
그 후 Express에서 ‘build’ 폴더를 정적 디렉토리로 지정하여 리액트 ‘index.html’을 사용할 것이다.
‘build’ 폴더 내부에 있든 JS파일을 이용하여 리액트가 우리의 화면을 그려줄 것이다.
즉, 리액트와 Express가 하나의 서버에 존재하는 것이다.
그럼 이제부터 생각하는 것을 이루어내기 위해 Server, Client 각각의 과정을 살펴보자.
서버 쪽 코드는 이미 express-generator를 통하여 서버를 실행시키는데 필요한 기본적인 것들이 갖추여져 있다.
const createError = require("http-errors");
const express = require("express");
const app = express();
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, "public")));
...
첫페이지는 서버에서 렌더링 된 html을 응답으로 주도록 하였다. (Sever Side Rendring)
app.get("/", (req, res) => {
res.render("index");
});
그 후 ‘/react’ 경로에는 리액트 앱을 사용한다. (리액트를 사용하는 경로라는 걸 명시하기 위해 사용한 url)
아래의 코드를 통해 리액트에서 빌드된 코드를 사용할 수 있게된다.
app.use("/react", express.static(path.join(__dirname, "build")));
그 후 ‘/react’ 경로에 접근하였을 때 ‘index.html’파일을 응답해준다.
app.use("/react", reactRouter);
reactRouter.get("/", function (req, res, next) {
res.sendFile(path.join(__dirname, "./build/index.html"));
});
그리고 서버 ‘/users’에 요청하면 MongoDB에 저장되어 있는 User 정보를 ‘JSON’으로 응답해주게 하였다.
app.use("/users", usersRouter);
usersRouter.get("/", async (req, res, next) => {
try {
const users = await User.find().lean();
return res.json(users);
} catch (error) {
next(createError(500));
}
});
클라이언트 쪽도 CRA 명령어를 통해 이미 기본적인것들이 갖추어진 상태이다.
여기에 ‘react-router-dom’을 이용하여 기본적인 리액트 앱을 구성해보자.
import { BrowserRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter basename="/react">
<App />
</BrowserRouter>
);
import React from "react";
import { Link, Route, Routes } from "react-router-dom";
import Home from "./Home";
import Users from "./Users";
function App() {
return (
<div>
<Link to="/">
Home
</Link>
<Link to="/users">
User
</Link>
<Routes>
<Route path="/" element={<Home />}></Route>
<Route path="/users" element={<Users />}></Route>
</Routes>
</div>
);
}
export default App;
그리고 ‘Users’ 컴포넌트에서 서버에 요청하여 DB에 저장되어 있는 유저 정보를 보여준다.
import React, { useEffect, useState } from "react";
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
const getUsers = async () => {
const response = await fetch("/users");
if (!response.ok) {
const message = `An error has occured: ${response.status}`;
throw new Error(message);
}
const users = await response.json();
return users;
};
getUsers().then((data) => setUsers((prev) => prev.concat(data)));
}, []);
return (
<>
<ul>
{users.map((user) => (
<li>{user.name}</li>
))}
</ul>
</>
);
}
fetch("/users")
를 통해 Express서버에 요청 (경로를 주목할 필요가 있다)그리고 리액트 ‘package.json’에서 기본 홈페이지 경로를 ‘/react’로 설정해 주었다.
지금부터 리액트와 Express를 합치기 위해 리액트에서 ‘build’ 스크립트를 통해 빌드된 폴더를 ‘server’ 폴더로 옮길것이다.
그리고 ‘Heroku’배포를 위해 필요한 명령어들을 정의해 주어야 한다.
그러기 위해서는 client, server 전체를 아우르는 ‘package.json’이 필요하다.
{
"name": "express-react-project",
"version": "1.0.0",
"description": "배포시 필요한 스크립트 정의",
"scripts": {
...
},
"author": "",
"license": "ISC"
}
이제 이 ‘package.json’파일을 통해 우리가 해야하는 일을 정의해보자.
위 과정을 명령어 스크립트로 정리해보자.
{
"name": "express-react-project",
"version": "1.0.0",
"description": "배포시 필요한 스크립트 정의",
"scripts": {
"client-build": "cd client && npm ci && npm run build cd ../",
"server-build": "cd server && npm ci && cd ../",
"heroku-prebuild": "npm run client-build && npm run server-build && mv ./client/build ./server/build",
"start": "cd server && npm run start"
},
"author": "June",
}
그럼 이제 Heroku 명령어를 통해 배포를 진행해보자.
성공적으로 배포가 된것을 확인할 수 있다.
서버에 데이터 흐름을 이해하기 쉽게 draw.io를 이용하여 그려보았다.
하나의 서버에서 Express, react가 구동되는 템플릿을 만들어 보았다.
두 가지를 따로 두고 구현했다면 CORS 정책을 신경써야 했지만 같은 ‘Origin’이기 때문에 리액트에서 Express에 요청을 보내는 것이 간단해졌다.
그리고 웹에서 첫페이지는 화면을 사용자에게 빠르게 보여주고, SEO에 강점이 있는 ‘SSR’, 그리고 내부에서는 페이지 새로고침이 없어 사용자 경험을 개선할 수 있는 ‘CSR’을 도입해 보았다.
그리고 만약 우리가 위에서 설정한 환경에서 클라이언트 리액트 코드를 개발할 때 리액트 ‘package.json’에 proxy 값을 Express 서버로 설정해 둔다면 문제없이 라이브 코딩이 가능할 것이다.
오늘 자료를 구성하면서 크게 느낀 부분은 JS언어 만으로도 풀스택 개발이 가능하다는 점이다.
다만, Express의 경우 유연하고 진입장벽이 낮은 장점은 있지만 서버 보안, 예외 처리 등을 위한 규칙이 존재하지 않는다고 느꼈다.
규칙이 없다는 것은 좋을 때도 있지만 서버와 같이 안전성을 위해 보수적으로 코드를 짜야하는 경우에는 규칙과 템플릿이 존재하는 것이 좋다고 생각한다. (Ex: Java Spring, Python django)
우리가 하고 있는 작은 프로젝트에서는 괜찮을 수도 있지만 실제 제품과 같은 큰 서버에서는 틀이 없다는 것은 단점으로 적용할 수도 있을 거 같다.
그래서 찾아보니 이미 NestJS 라는 대체제가 존재한다고 한다!
Documentation | NestJS - A progressive Node.js framework
앞으로의 개발에서 오늘 만든 Express + React 템플릿을 자주 사용할 수 있을 거 같다.
[Node.js] Express를 backend로 하는 Create-react-app 시작하기
Create React App with an Express Backend