프론트와 백엔드를 통합하기 위해 첫번째로 할 일은 Todo 아이템을 불러오는 것.
즉 http://localhost:3000 으로 접속 시, Todo 아이템이 리스트에 보여야한다. 이를 구현하기 위해 백엔드 서버에 작업 (fetch 함수를 이용하여 백엔드 서버에 요청 전송하기)
리액트는 브라우저에 보이는 HTML DOM 트리와는 다른 Virtual DOM을 가지고 있고, 컴포넌트의 State가 변하면 리액트의 Virtula DOM은 이를 감지하고 변경된 부분의 HTML을 바꿔준다. HTML이 업데이트 되는 것을 렌더링이라고 한다.
클래스형 컴포넌트의 경우
렌더링이 가장 처음 일어나는 순간 (=ReactDOM, 즉 Virtual DOM이 존재하지 않는 순간) 은 리액트 엔진이 처음으로 각 컴포넌트를 render해주는 과정을 mounting이라고 한다
마운팅과정에서 생성자와 render()함수를 부른다. 그리고 마운팅을 마친 후 바로 호출하는 하나의 함수가 더 있는데, 이는 componenetDidMount
함수다.
http://localhost:3000 으로 접속 시, Todo 아이템이 리스트에 보여야하니까
componenetDidMount
함수 내부에 백엔드 API 콜을 구현하면 된다.
그런데!! 함수형 컴포넌트는 `componenetDidMount`가 없다!!! 대신에!!! useEffect를 쓰면 된다!
근데 왜 생성자에서 API 콜을 하지 않을까?
➡️ 마운팅이 다 안되었다는 것은 컴포넌트의 property가 준비되지 않은 상태라는 뜻이기에
어쨌든 useEffect와 fetch를 사용해서 백엔드와 프론트를 통합하자
App.js
import React, {useState, useEffect} from 'react';
import './App.css';
function App() {
useEffect(() => {
const requestOptions = {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
};
fetch('http://localhost:8080/todo', requestOptions)
.then((response) => response.json())
.then((response) => {
console.log(response.data);
});
}, []);
위 코드를 작성한 후 localhost:3000에서 콘솔 확인하면 아래와 같다.
➡️ 보안을 위한 CORS 헤더 Policy를 위반하여서 나타나는 에러
(Cross-Origin Resource Sharing)
처음 리소스를 제공한 도메인 (origin)이 현재 요청하려는 도메인과 달라도 요청을 허락해주는 웹 보안 방침
CORS 방침으로 요청이 거래되는 과정을 그림으로 나타나면 아래와 같다.
현재 프로젝트 TODO APP의 프론트엔드 서버 도메인은 http://localhost:3000
이기 때문에, 현재 Todo 페이지의 origin은 http://localhost:3000
이다. 그래서 useEffect에서 fetch로 http://localhost:8080
으로 요청을 보냈을 때 접근이 거부된 것
CORS가 가능하려면 백엔드에서 설정해줘야한다.
WebMvcConfig.java
package com.example.demo.config;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// 스프링 빈으로 설정해줄것
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final long MAX_AGE_SECS = 3600;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(MAX_AGE_SECS);
}
}
/**
)에 대해 Origin이 http://localhost:3000
인 경우 GET, PUT, DELETE, POST, PATCH, OPTIONS 메서드를 이용한 요청 허용fetch는 Promise를 리턴한다. Promise는 비동기 작업에서 사용한다. Promise는 해당 함수를 실행 한 후, Prommise 오브젝트에 명시된 사항들을 실행해준다. Promise에는 Pending, Resolve, Reject 세 가지 상태가 존재한다.
resolve()
함수를 통해 이 함수가 성공적으로 끝났음을 알리고, 원하는 값을 전달한다.resolve()
는 then의 매개변수로 넘어오는 함수를 실행한다.reject()
는 에러 났을 때 실행하고, catch의 매개변수를 실행한다.➡️ then이나 catch는 바로 실행되는 것이 아니고, 매개변수로 해야할 일을 넘겨주기만 한다. 실제로 then 또는 catch가 실행되느 시점은 resolve와 reeject가 시행되는 때이다.
fetch는 API서버로 http 요청을 송신 및 수신할 수 있도록 지원하는 자바스크립트 메서드이다. 매개변수로 url을 받거나, url과 options를 받는다. fetch()
의 리턴값은 Promise이기 때문에, then과 catch에 콜백 함수를 전달해서 응답을 처리할 수 있다.
// 디폴트 http 메서드는 GET
fetch("localhost:8080/todo")
.then(response => {
// 수신 시 작업 ..
})
.catch(e=>{
// 에러 시 작업
})
options = {
method : "POST",
headers: [
['Content-Type': 'application/json']
],
body : JSON.stringify(data)
};
fetch("localhost:8080/todo", options)
.then(response => {
// 수신 시 작업 ..
})
.catch(e=>{
// 에러 시 작업
})
fetch("localhost:8080/todo", options)
하드코딩 🙅🏻♀️
설정파일에서 URI 가져오도록 구현하여 도메인 바뀌어도 적용할 수 있도록한다.
프론트엔드 프로젝트 src 디렉터리 내부에 app-config.js 파일 생성
let backendHost;
const hostname = window && window.location && window.location.hostname;
if (hostname === 'localhost') {
backendHost = 'http://localhost:8080';
}
export const API_BASE_URL = `${backendHost}`;
src 디렉터리 내부에 생성한다. 백엔드로 요청을 보낼 때 사용하는 유틸리티 함수 작성
import { API_BASE_URL } from '../app-config';
export function call(api, method, req) {
const options = {
headers: new Headers({ 'Content-Type': 'application/json' }),
url: API_BASE_URL + api,
method: method,
};
if (req) {
options.body = JSON.stringify(req);
}
return fetch(options.url, options).then((res) => {
res.json().then((json) => {
if (!res.ok) {
return Promise.reject(json);
}
return json;
});
});
}
import React from 'react';
import { useEffect, useState } from 'react';
import TodoItem from './components/TodoItem';
import AddTodo from './components/AddTodo';
import { call } from './services/ApiService';
import Header from './components/Header';
import Loader from './components/Loader';
const uuid = require('uuid');
const App = () => {
const [items, setItems] = useState([]);
useEffect(() => {
call('/todo', 'GET', null).then((res) => {
setItems(res.data.data);
});
}, []);
const add = (item) => {
const id = uuid.v4();
const newItem = { id, title: item, done: false };
setItems(items.concat(newItem));
call('/todo', 'POST', newItem).then((res) => {
setItems(res.data.data);
});
};
const deleteItem = (item) => {
const itemId = { id: item };
call('/todo', 'DELETE', itemId).then((res) => {
setItems(res.data.data);
});
};
const update = (item) => {
console.log(item);
call('/todo', 'PUT', item).then((res) => {
setItems(res.data.data);
});
};
const todoItems =
items.length > 0 &&
items.map((item, idx) => (
<TodoItem
item={item}
key={item.id}
deleteItem={deleteItem}
update={update}
/>
));
const content = (
<div>
<Header />
<div className="todo-container">
<AddTodo add={add} />
<ul>{todoItems}</ul>
</div>
</div>
);
return <div>{content}</div>;
};
export default App;