자바스크립트로 풀스택 개발을 할 땐 흔히 프론트엔드로 React.js, 백엔드로 Express.js를 사용하게 한다.
일반적으로 개발 과정에서는 React 프론트엔드와 Express 백엔드 서버를 동시에 실행시키고, 클라이언트 측 코드에서 백엔드로 요청을 보내고 응답을 받는 방식을 사용한다.
중요한 점은, Express는 요청에 포함된 JSON 형태의 데이터를 받으면 응답을 JSON 형태로 반환하는 역할만을 수행한다. 화면에 띄워주는 건 React가 해 준다.
이때 서버로 GET, POST
등의 API 요청을 보내주는 패키지가 axios
다. 이를 사용하는 방법을 다루어보겠다.
Express 서버가 3000번 포트로 돌아가고, API 요청이 /foods
경로에서 이루어질때, 아래와 같이 React 쪽 코드에 프록시 서버 설정을 해 주어야 한다.
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
// 이걸 해 주셔야 합니다.
server: {
proxy: {
"/foods": "http://localhost:3000",
},
},
// 감사합니다.
});
이렇게 설정을 해 주면, React 쪽에서 /foods
경로로 요청을 보낼 때 자동으로 http://localhost:3000
(Express 서버)로 전달된다.
이 설정 없으면, Express 서버 말고 React 개발서버 자기 자신에게 요청을 보내는 오류가 발생하니까 꼭 꼭 꼭 해 두자.
게시판이나 쇼핑사이트 같은 사이트를 구현할 땐, 데이터베이스에 저장된 모든 데이터(게시글이든, 상품 정보든)를 화면에 표시해야 할 거다.
일단 Express 서버에선, /foods
로 GET요청이 들어오면, MongoDB의 Food
컬렉션에 저장된 모든 도큐먼트를 JSON 형태로 응답하도록 구성할 수 있다.
// Express [서버]
app.get('/foods', async (req, res) => {
try {
// Food 콜렉션의 모든 도큐먼트를 반환한다.
const foods = await Food.find();
// 이를 json 형태로 응답한다.
res.status(200).json(foods);
} catch (error) {
res.status(500).json({ message: '서버 오류', error: error.message });
}
});
이런 코드로 요청을 보내면, 아래와 같은 도큐먼트 객체의 배열 형태로 응답한다.
// {}로 구성된 키, 값으로 구성된 애들을 객체라고 한다.
[
{ "_id": "64d3a2...", "name": "사과", "price": 1000, "category": "과일" },
{ "_id": "64d3a3...", "name": "바나나", "price": 1500, "category": "과일" }
]
문제는 React에서 이러한 GET 요청을 보내고 응답을 받을 방법이 필요하단 거다. 그 역할을 axios가 해 준다.
// React [클라이언트]
import axios from "axios";
const API_URL = "/foods";
// 전체 조회
export async function getAllFoods() {
// /foods 경로로 GET 요청.
const res = await axios.get(API_URL);
// 응답데이터를 반환
return res.data;
}
axios를 활용해, 서버에 GET 요청을 보내는 getAllFoods
함수를 만들어 보자.
단순히 axios.get(요청경로)
를 이용해 GET 요청을 보낼 수 있다. async
/await
사용했으니, 서버로부터 응답이 올 때까지 return res.data
가 실행되지 않는다.
이후 return res.data
는, 서버에서 보낸 JSON 데이터를 그대로 반환한다. 이때 별도로 json 형태로 parse해야한다거나 할 필요는 없다. axios에서 알아서 다 해준다.
그러면 반환값은 앞서 본 도큐먼트 객체의 배열가 된다.
사이트를 새로 접속했거나, 새로고침했을 때, 데이터베이스의 모든 값을 나열해서 화면에 띄우고 싶다고 하자.
리액트의 App 컴포넌트에서, 화면에 띄울 내용을 배열 형태의 state로 관리한다고 가정한다.
일단 초기값은 빈 배열로 놔두되, App 컴포넌트가 처음으로 렌더링될 때 서버에 GET 요청을 보내 모든 데이터를 가져와야 한다.
// React [프론트엔드]
function App() {
const [foods, setFoods] = useState([]);
// []: App이 처음으로 렌더링될 때만, 콜백 함수를 실행한다.
useEffect(() => {
getAllFoods().then(setFoods);
}, []);
이럴 땐 useEffect
라는 친구를 쓸 수 있다. 일반적으론 두번째 인자에 state를 넣으면, 해당 값이 바뀔때마다 첫번째 인자의 콜백함수를 실행한다.
그런데 두번째 인자에 []
를 넣는 경우, 컴포넌트가 처음으로 렌더링될 때만 콜백함수를 실행한다.
콜백함수로 getAllFoods().then(setFoods)
를 두면
getAllFoods()
로, DB에 GET 요청을 보내 데이터를 가져온다.then(setFoods)
로 foods
state를 업데이트한다이후엔 foods
배열의 값을 적절한 방식으로 렌더링하게끔 App
을 구현하면, 서버에서 받은 데이터를 화면에 표시할 수 있다. 신난다!
POST요청과 같이 JSON 데이터를 서버로 보낼 때가 있는데,
단순히 axios.post()
함수의 두번째 인자로 객체를 넣어주면 된다.
// React [프론트엔드]
// 생성
export async function createFood(name, price, category) {
const res = await axios.post(API_URL, { name, price, category });
return res.data;
}
위 createFood
함수는 매개변수로 받은 name
, price
, category
를 JSON 객체로 만들어 /foods
경로로 POST 요청을 보내는 역할을 한다.
// Express [백엔드]
// JSON 데이터를 받을 수 있도록 설정
app.use(express.json());
// POST 요청 처리
app.post("/foods", (req, res) => {
// req.body로 전달받은 JSON 객체에 접근할 수 있다.
const { name, price, category } = req.body;
// 이후 Food 데이터베이스에 삽입하는 등 후속 처리를 해주면 되겠다.
const food = await Food.create({ name, price, category });
res.status(201).json(food);
}
이런 식으로 객체를 보내주면, req.body
를 이용해 접근할 수 있다.
Express에서는 JSON 응답을 받기 위해선 app.use(express.json())
미들웨어를 설정해 두어야 한다는 점을 기억해 두자.
실제 테스트는 React 서버와 Express 서버 양쪽 모두 실행한 뒤,
웹 브라우저에서 React 앱을 열고 이것저것 시도해보는 방식으로 할 수 있다.
npm run build
를 쉘에서 실행하면, dist/
폴더가 생성된다.
프론트엔드(리액트)쪽 html, css, js 코드가 포함된다. 앞선 REact 서버는 개발 편의를 위해 존재한 거고, 실제 배포할 땐 React 쪽은 서버를 쓰지 않는다.
대신 서버 쪽에서 dist/
폴더에 있는 파일들을 인식할 수 있게 처리를 해야 한다.
// Express [백엔드]
import express from "express";
import path from "path";
const app = express();
// JSON 요청 처리
app.use(express.json());
// **추가할부분 1**
// React 정적 파일 제공
// __dirname: 현재 절대경로를 뜻하는 전역변수.
// 해당 폴더의 css, js, 이미지 등 정적 파일을 express가 사용할 수 있게 설정한다.
app.use(express.static(path.join(__dirname, "dist")));
// 기존에 만들어 둔 API 라우트
app.get("/foods", async (req, res) => {
const foods = await Food.find();
res.json(foods);
});
app.post("/foods", async (req, res) => {
const { name, price, category } = req.body;
const food = await Food.create({ name, price, category });
res.status(201).json(food);
});
// **추가할부분 2**
// React SPA 처리
// Express에서 설정하지 않은 aPI 요청의 경우, 프론트엔드 쪽 index.html로 보내 React가 처리하게 함
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "dist", "index.html"));
});
app.listen(3000, () => console.log("Server running on 3000"));
사실 이 부분은 아직 배포를 할 일이 없어서 간단히만 적었는데,
추가할부분 1에 표시한 대로, dist
폴더에 저장된 정적 파일을 express가 사용할 수 있게 미들웨어를 선언해야 한다.
그리고 추가핧부분 2에 표시한 대로, Express에서 정의하지 않은 경로의 경우 dist/index.html
로 보내 React에서 처리할 수 있게 한다.
*
는 어떤 경로든 콜백함수대로 실행하게끔 처리한다는 뜻이다."/foods"
로 API 요청이 먼저 갔는지 확인하고, 아닌 경우에만 React 쪽으로 넘겨야 한다.