React에서 API 통신을 할 때에도, 기존 자바스크립트 환경에서 사용하던 fetch
와 axios
를 그대로 사용하는데, 이 두가지 방법 모두 useEffect
와 Promise
를 사용한다. 여기서 useEffect는 컴포넌트가 렌더링될 때 실행될 코드를 정의하는 Hook으로, 페이지가 처음 로드될 때 실행할 코드를 정의하거나 서버에서 데이터를 받아오기 위한 목적으로 사용된다. 또한 Promise 객체는 서버와의 비동기 통신(async/await)을 목적으로 사용된다.
fetch 메서드는 별도의 모듈 import 없이 바로 사용 가능하다. fetch를 사용할 때 주의해야 할 점은 반드시 fetch 메서드로 받아온 data를 Json 객체로 변환 후 반환해야 한다는 것이다. API 통신을 구현하기 위해 해야 할 일은 아래와 같다.
① API의 input 입력 받기
② API 호출 결과 저장하기
지금부터 fetch()를 이용한 API 통신을 실제로 구현해보자. 아래는 관리자 페이지에 DB 연결 정보를 입력 후 '연결하기' 버튼을 클릭하면, 해당 DB로의 연결을 시도하는 API를 호출하는 예제이다.
import React, { useState, useEffect } from 'react';
...
function DBTestPage(props) {
// 사용자 입력 값을 저장
const [formData, setFormData] = useState({
host: '',
port: '',
username: '',
password: '',
sid: '',
connectString: ''
});
// API 호출 결과를 저장
const [apiResult, setApiResult] = useState('연결하기 버튼을 눌러 DB 연동 상태를 확인할 수 있습니다.');
const fields = [
{ type: 'text', id: "host", label: 'HOST', placeholder: '호스트의 IP 주소를 입력하세요' },
{ type: 'number', id: "port",label: 'PORT', placeholder: '포트 번호를 입력하세요' },
{ type: 'text', id: "username",label: 'USER_NAME', placeholder: 'DB 사용자 계정을 입력하세요' },
{ type: 'password', id: "password",label: 'PASSWORD', placeholder: 'DB 패스워드를 입력하세요' },
{ type: 'text', id: "sid",label: 'SID', placeholder: 'SID를 입력하세요' },
{ type: 'text', id: "connectString", label: 'CONNECT_STRING', placeholder: 'Connect String을 입력하세요' }
];
// 사용자 입력 값이 변경될 때마다 호출되는 CallBack 메서드
const handleChange = (e, id) => {
setFormData(prevState => ({
...prevState,
[id]: e.target.value // 특정 key에 대응하는 value를 업데이트
}));
};
// 사용자 입력 값 로깅
useEffect(() => {
console.log(formData);
}, [formData]);
// API 호출 및 API 응답 수신
const handleSubmit = async (e) => {
e.preventDefault();
const response = await fetch('http://localhost:3000/db/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const result = await response.json();
if (!response.ok) {
const parsedJson = JSON.parse(JSON.stringify(result));
setApiResult(parsedJson.message);
return;
}
const parsedJson = JSON.parse(JSON.stringify(result));
setApiResult(parsedJson.message);
};
return (
<Wrapper>
...
<div>
{fields.map((field) => (
<FlexContainer key={field.key}>
<ConnectDBText>{field.label}:</ConnectDBText>
<TextInput
type={field.type}
height={25}
width={300}
placeholder={field.placeholder}
onChange={(e) => handleChange(e, field.id)}
/>
</FlexContainer>
))}
</div>
<Button
title='연결하기'
style={{ marginTop: '20px', marginLeft: '480px' }}
onClick={handleSubmit}/>
<WhiteBackgroundDiv>
<ApiText>{apiResult}</ApiText>
</WhiteBackgroundDiv>
...
export default DBTestPage;
아래는 실행 화면이다.
① 초기 화면
② DB 연결 성공
③ 예외 처리
axios를 사용하기 위해서는 모듈 설치와 import 구문이 필요하다.
# 모듈 설치
npm install axios
# import 구문
import axios from 'axios';
일반적으로 fetch 메서드보다는 axios 메서드의 사용이 더 권장된다. 그 이유는 Json 변환 과정을 수동적으로 처리해야하는 fetch()와는 달리, axios()에서는 Json 변환을 자동으로 처리해주기 때문이다. 또한, fetch()는 네트워크 에러를 제외한 다른 에러를, 예외로 판단하지 않기 때문에 예외 처리가 axios에 비해 다소 복잡하다.
이제 위에서 사용한 코드를 axios를 이용한 코드로 변경해보자. (fetch를 사용할 때와 동일한 부분은 제외하여 나타내었다.)
import axios from 'axios';
...
// API 호출 및 API 응답 수신
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await axios.post('http://localhost:3000/db/test', formData, {
headers: {
'Content-Type': 'application/json'
}
});
// fetch와 달리 Json 변환 없이, 바로 response.data로 결과 데이터를 사용할 수 있음.
setApiResult(response.data.message);
} catch (error) {
// 에러 발생 시에도 Json 변환 없이, 바로 error.response로 예외 객체를 사용할 수 있음.
// fetch 보다 예외 처리 로직이 간결함.
setApiResult(error.response.data.message);
}
...
CORS는 Cross Origin Resource Sharing의 약자로, 브라우저에서 보안 상의 이유로 Cross Origin Request를 제한하는 기능이다. Cross Origin의 개념을 이해하기 위해선, 먼저 Origin이 무엇인지 알아야 한다. Origin은 URL 중 Protocol, Domain(Host), Port에 해당하는 부분이다. 즉, 이 3개의 요소가 동일한 경우에만 같은 출처로 간주된다는 것이다. 다시 말해, Cross Origin Request는 아래의 조건 중 하나에 해당하는 요청인 것이다.
그렇다면 CORS가 필요한 이유는 무엇일까? 브라우저는 기본적으로 SOP(Same-Origin Policy) 정책을 따른다. 즉, 같은 출처의 리소스 공유만 원칙적으로 허용되는 것이다. 그러나, SOP를 너무 엄격하게 적용해버리면, 다른 출처 간의 상호작용이 원천적으로 금지되기 때문에 다른 출처의 외부 리소스를 아예 사용할 수 없게 되어버린다. 이러한 경우를 대비하기 위하여 특별히 CORS 정책을 만족하는 요청만큼은 출처가 다르더라도 요청을 허용해주게 된 것이다.
그런데 다른 출처의 리소스에 접근하는 것을 왜 제한해야 할까? 만약 다른 출처의 리소스에 접근하는 것이 자율적으로 허용될 경우, 아래와 같은 문제가 발생할 수 있다.
Node.js와 React.js 모두 localhost에서 동작하고 있고, 기본 포트인 3000번 포트를 사용하여 개발 환경을 구축하였다. 이와 같이 개발 환경을 구축할 경우, 아래와 같은 문제점이 발생하게 된다.
① 동일한 포트(3000번)에서 Node.js를 먼저 실행한 이후 React.js 실행
② 다른 포트에서 Node.js(3000번)를 먼저 실행한 이후 React.js(3001번) 실행
③ 동일한 포트(3000번)에서 React.js를 먼저 실행한 이후 Node.js 실행
CORS 설정은 프론트엔드와 백엔드에서 모두 할 수 있지만, 여기서는 프론트엔드에서 CORS를 설정하는 방법에 대해서만 다루기로 한다. 또한, 여기서는 Node.js의 포트는 3000으로 유지한 상태에서, React.js의 포트를 5000으로 변경하여 포트 충돌을 회피하도록 하겠다. (물론, React.js의 포트를 유지하고, Node.js의 포트 번호를 변경해도 상관 없다.)
① React의 포트 번호를 변경한다.
set PORT=5000 &&
를 추가한다."scripts": {
"start": "set PORT=5000 && react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
② 계속해서 package.json 파일에 아래의 내용을 추가한다.
"proxy": "http://localhost:3000",
③ API 호출 부분을 아래와 같이 수정한다.
http://localhost:3000/db/test
를 /db/test
로 변경한다.// fetch()
const response = await fetch('/db/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
// axios()
const response = await axios.post('/db/test', formData, {
headers: {
'Content-Type': 'application/json'
}
});
브라우저에서 API를 호출하면, React의 Proxy 서버로 요청이 전달되고, Proxy 서버는 Node.js 서버와 동일한 Origin을 갖기 때문에 API를 성공적으로 호출할 수 있게 된다. 당연히 API 요청에 대한 응답도 브라우저에 직접 전달되지 않고, React의 Proxy 서버를 통해 브라우저로 전달된다. 결과적으로, 브라우저는 오직 Proxy 서버와만 통신할 뿐 백엔드 서버와는 직접적으로 통신하지 않기 때문에 CORS를 우회할 수 있게 된다. 이로써, 포트 충돌과 CORS로 인한 API 호출 문제를 모두 해결할 수 있게 된다.
React 개발 서버와 Proxy 서버의 포트 번호가 다른데 어떻게 CORS를 우회할 수 있는지 의문이 생길 수도 있다. 우회가 가능한 이유는 브라우저의 입장에서 React 개발 서버와 Proxy 서버는 동일하게 간주되기 때문이다. 즉, 실제로는 브라우저와 Proxy 서버 간의 통신이지만, 브라우저 입장에서는 그저 React.js와의 일반적인 통신 프로세스일 뿐이다.