4월 2~4주차. React와 API 연동하기 / CORS

변현섭·2024년 4월 22일
0

다우데이타 인턴십

목록 보기
13/17

1. 사전 지식

React에서 API 통신을 할 때에도, 기존 자바스크립트 환경에서 사용하던 fetchaxios를 그대로 사용하는데, 이 두가지 방법 모두 useEffectPromise를 사용한다. 여기서 useEffect는 컴포넌트가 렌더링될 때 실행될 코드를 정의하는 Hook으로, 페이지가 처음 로드될 때 실행할 코드를 정의하거나 서버에서 데이터를 받아오기 위한 목적으로 사용된다. 또한 Promise 객체는 서버와의 비동기 통신(async/await)을 목적으로 사용된다.

2. fetch()

fetch 메서드는 별도의 모듈 import 없이 바로 사용 가능하다. fetch를 사용할 때 주의해야 할 점은 반드시 fetch 메서드로 받아온 data를 Json 객체로 변환 후 반환해야 한다는 것이다. API 통신을 구현하기 위해 해야 할 일은 아래와 같다.

① API의 input 입력 받기

  • 사용자가 입력해야 하는 값이 2개 이상이라면, form의 형태로 입력 값을 저장한 후 해당 폼을 API의 input으로 전달해야 한다.
  • 사용자의 입력이 변경될 때마다 form을 업데이트하기 위해 useState 훅을 사용한다.

② API 호출 결과 저장하기

  • API를 호출할 때마다 다른 결과가 나타나야 하므로, API의 output을 보여줄 때에도 useState 훅을 사용한다.

지금부터 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 연결 성공

③ 예외 처리

3. axios()

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);
            
        }
        ...
        

4. CORS

1) 의미

CORS는 Cross Origin Resource Sharing의 약자로, 브라우저에서 보안 상의 이유로 Cross Origin Request를 제한하는 기능이다. Cross Origin의 개념을 이해하기 위해선, 먼저 Origin이 무엇인지 알아야 한다. Origin은 URL 중 Protocol, Domain(Host), Port에 해당하는 부분이다. 즉, 이 3개의 요소가 동일한 경우에만 같은 출처로 간주된다는 것이다. 다시 말해, Cross Origin Request는 아래의 조건 중 하나에 해당하는 요청인 것이다.

  • 프로토콜이 다른 경우
  • 도메인이 다른 경우
  • 포트 번호가 다른 경우

2) 필요성

그렇다면 CORS가 필요한 이유는 무엇일까? 브라우저는 기본적으로 SOP(Same-Origin Policy) 정책을 따른다. 즉, 같은 출처의 리소스 공유만 원칙적으로 허용되는 것이다. 그러나, SOP를 너무 엄격하게 적용해버리면, 다른 출처 간의 상호작용이 원천적으로 금지되기 때문에 다른 출처의 외부 리소스를 아예 사용할 수 없게 되어버린다. 이러한 경우를 대비하기 위하여 특별히 CORS 정책을 만족하는 요청만큼은 출처가 다르더라도 요청을 허용해주게 된 것이다.

그런데 다른 출처의 리소스에 접근하는 것을 왜 제한해야 할까? 만약 다른 출처의 리소스에 접근하는 것이 자율적으로 허용될 경우, 아래와 같은 문제가 발생할 수 있다.

  • 정보 유출: 사용자의 개인 정보가 다른 출처로 유출될 수 있다.
  • 인증 정보 탈취: XSS나 CSRF 공격 등에 쉽게 노출될 수 있다.
    • XSS(Cross Site Scripting): 악의적인 스크립트를 삽입하여 인증된 사용자의 세션이나 토큰 정보를 탈취하는 공격
    • CSRF(Cross Site Request Forgery): 인증된 사용자의 권한을 이용하여 악의적인 요청을 서버에 보내는 공격
  • DOS 공격: 다른 출처에서 고의적으로 많은 요청을 보내 서버를 과부하시킬 수 있다.

5. CORS 해결

1) 문제점

Node.js와 React.js 모두 localhost에서 동작하고 있고, 기본 포트인 3000번 포트를 사용하여 개발 환경을 구축하였다. 이와 같이 개발 환경을 구축할 경우, 아래와 같은 문제점이 발생하게 된다.

① 동일한 포트(3000번)에서 Node.js를 먼저 실행한 이후 React.js 실행

  • 포트 충돌로 인해 동일한 포트에서 실행할 수 없다는 메시지가 출력된다.

② 다른 포트에서 Node.js(3000번)를 먼저 실행한 이후 React.js(3001번) 실행

  • ① 번 질문에 Y로 답하면, localhost:3001에서 웹 페이지가 실행된다.
  • 웹 페이지는 잘 실행되지만, 서버의 주소인 localhost:3000으로 API 요청을 보내면 CORS 정책에 의해 API 요청이 Block 되었다는 에러 메시지가 출력된다. (포트번호가 다르기 때문에 Cross Origin으로 간주된다.)

③ 동일한 포트(3000번)에서 React.js를 먼저 실행한 이후 Node.js 실행

  • 웹 페이지는 잘 실행되지만, API가 호출되지 않는 문제가 발생한다. (404 Not Found Error)

2) 해결 방법

CORS 설정은 프론트엔드와 백엔드에서 모두 할 수 있지만, 여기서는 프론트엔드에서 CORS를 설정하는 방법에 대해서만 다루기로 한다. 또한, 여기서는 Node.js의 포트는 3000으로 유지한 상태에서, React.js의 포트를 5000으로 변경하여 포트 충돌을 회피하도록 하겠다. (물론, React.js의 포트를 유지하고, Node.js의 포트 번호를 변경해도 상관 없다.)

① React의 포트 번호를 변경한다.

  • package.json 파일의 scripts > start 부분에 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 파일에 아래의 내용을 추가한다.

  • 아래의 3000은 서버의 포트 번호에 알맞게 변경한다.
  • 이 설정은 브라우저에서 직접 API를 호출하지 않고, 리액트의 Proxy 서버로 요청을 전달하기 위한 설정이다.
  • 브라우저에서 직접 API를 호출하지 않기 때문에 CORS를 우회할 수 있게 되며, Proxy 서버는 Node.js와 동일한 origin이므로, SOP를 만족하게 된다(API가 정상적으로 호출된다).
"proxy": "http://localhost:3000",

③ API 호출 부분을 아래와 같이 수정한다.

  • 호출 URL을 절대경로에서 상대경로로 변경해야 한다. 즉, 기존 http://localhost:3000/db/test/db/test로 변경한다.
  • 상대 경로의 경우, 현재 호스트와 포트 번호를 기준으로 URL을 해석하기 때문에, 요청을 Prxoy 서버로 전달할 수 있게 된다. 하지만, 절대 경로의 경우 지정된 URL을 그대로 사용하므로, Proxy 설정을 사용할 수 없다.
// 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와의 일반적인 통신 프로세스일 뿐이다.

profile
LG전자 Connected Service 1 Unit 연구원 변현섭입니다.

0개의 댓글