현재까지는 더미 데이터로 로컬에서 데이터를 받아아 작업을 했다면, 이제 리액트 앱을 어떻게 백엔드/데이터베이스에 연결하는지 알아 보자. 앱에서 데이터를 데이터베이스에서 불러오거나 저장하기 위해, 리액트 엡에서 백엔드로 HTTP 요청 보내는 방법을 알아보자.
리액트 앱이 데이터베이스와 소통하는 방법
(How do React Apps interact with DB?)
HTTP 요청 보내는 방법 및 HTTP 요청에 대한 응답을 처리하는 방법
(Sending HTTP Requests & Using Responses)
앱에서 로딩 상태를 처리하는 방법 및 앱에서 오류 처리하는 방법
(Handling Loading State & Errors)
리액트 앱이나 일반적인 브라우저 앱에서는, 브라우저에서 실행되는 JS 코드가 DB와 직접 통신하면 절대로 안되기 때문에 내가 생각하는 것 처럼 작동하지 않을 수도 있다.
데이터베이스를 데이터베이스 서버에서 실행하는 것은 문제가 안되지만, 앱으로 직접 데이터를 가져오거나 저장하고 연결을 맺는 행위 등을 외부 환경에서 절대로 하면 안된다.
클라이언트 내부에서 데이터베이스에 직접 연결하거나, 브라우저의 JS 코드를 통해 직접 연결한다면 이는 이 코드를 통해 DB의 인증 정보를 노출시키는 행위이다.
잊지 말자! 브라우저에 실행되는 모든 JS 코드는 브라우저 뿐만 아니라 웹사이트의 모든 사용자들이 접근하고 읽을 수 있다. 간단히 개발자 도구를 열어서 모든 코드를 볼 수 있다.
코드가 공개되는건 일반적으로 문제되지는 않지만, 보안과 관련된 세부 사항이 담긴 코드를 노출하면 문제가 된다.
DB에 직접 접근하는 것은 성능 문제 같은 다른 이슈를 발생시킬 수 있지만 무엇보다도 보안 이슈 사항이 가장 큰 문제다. 따라서 리액트 앱 코드 내부에서 DB에 직접적으로 통신하는 것 대신 다른 방법을 사용해야 한다.
Backend App(NodeJS App, PHP App)이 있다고 하자. 백엔드 앱은 브라우저 안에서 실행되지 않고 다른 서버에서 실행된다. DB와 같은 서버일수도 있고 보통은 다른 서버이다. DB와 소통하는 백엔드 앱은 사용자가 이 백엔드 코드를 확인할 수 없기 때문에 DB의 인증 정보를 안전하게 저장할 수 있다. 다른 서버에 코드가 있기 때문에 웹사이트 사용자는 이 코드를 절대 볼 수 없다.
따라서 리액트는 해당 백엔드 서버, 일반적으로 서로 다른 URL 요청을 전송할 수 있는 백엔드 API와 통신한다.
인증 정보는 백엔드 앱에 저장되고, 백엔드 앱과의 통신은 보안에 관련된 세부사항이 필요 없으므로 데이터베이스와 안전하게 통신을 주고 받을 수 있다.
HTTP 요청에 대한 API를 말할 때는 보통 REST API 또는 GraphQL API를 말하게 된다.
이 둘은 서버가 데이터를 노출하는 방식에 대한 서로 다른 표준이다.
참고
최신 웹 개발은 API에 크게 의존한다. 프런트엔드 웹 앱(예: Angular 또는 React 사용)을 구축하든 모바일 앱을 구축하든 관계없이 일반적으로 데이터를 보내고 데이터를 검색할 수 있는 백엔드가 필요하다. 웹 API는 이러한 백엔드 역할을 한다. 이를 구축할 때 일반적으로 REST API와 GraphQL API라는 두 가지 주요 옵션이 있다.
잊지 말아야 할 사실은 리액트에서 우리가 작성하는 것은 정규 JS 코드라는 사실이다. 리액트 앱은 결국에는 JS 앱이다. 이것이 본질이다.
그렇기 때문에 JS 솔루션을 통해 우리가 원하는 HTTP 요청을 전달할 수 있다.
예를 들어 axios 패키지
가 있는데, 이는 어떤 JS 라이브러리를 사용하는 가에 상관 없이 HTTP 요청 전송을 하고, 이에 대한 반응을 매우 간단하게 할 수 있는 패키지이다.
하지만 최근에는 JS 내에서 HTTP 요청을 전송하는 내장 매커니즘인 Fetch API
가 있다. Fetch API는 브라우저 내장형이며 데이터를 불러오고 이름과는 다르게 데이터 전송도 가능하다. Fetch API를 통해 HTTP 요청을 전송하고 응답도 처리할 수 있다.
Fetch API를 사용하여 HTTP 요청을 보내보자.
해야 할 것
1. 버튼이 클릭될 때 마다 영화 정보를 가져온다.
2. 그 영화정보를 화면에 표시한다.
이를 위해 fetchMoviesHandler 라는 함수를 추가하여 FecthAPI를 사용해보자.
취득하려는 리소스를 정의하는데 다음 중 하나를 사용하면 된다.
요청에 적용하고자 하는 사용자 지정 설정을 포함하는 객체로 사용 가능한 설정은 다음과 같다.
method
GET, POST 등 요청 메서드
headers
요청에 추가하고자 하는 헤더들로 Headers 객체에 넣어 제공할 수도 있고, String 값들을 가진 객체 리터럴로 제공해도 된다.
body
요청에 추가하고자하는 본문이다.
mode
요청에 사용할 모드로, cors, no-cors, same-origin 이다.
반환값
Response 객체로 이행하는 Promise이다.
여기서는 기본적으로 GET메소드를 사용하기 때문에 get 메소드를 따로 적어주지 않아도 된다.
const fetchMoviesHandler = () => {
fetch("https://swapi.dev/api/films");
});
fetch() 함수는 Promise라는 객체를 반환하는데, 이 객체는 우리가 잠재적으로 발생할 수 있는 오류나 호출에 대한 응답에 반응할 수 있게 해준다.
위 영화 API가 반환하는 프로미스 객체는 어떤 즉각적인 행동 대신 영화 데이터를 전달하는 객체이다.
response.body
로 응답 객체의 body 본문을 읽어 올 수 있다.const fetchMoviesHandler = () => {
fetch("https://swapi.dev/api/films").then(response => {
return response;
});
});
""
로 묶여 있다.json()
이다. 이는 response 객체가 있는 내장 메소드이다. 이 메소드가 JS 객체로 변환 작업을 해준다.const fetchMoviesHandler = () => {
fetch("https://swapi.dev/api/films").then(response => {
return response.json();
});
});
then()
구역을 생성해야 한다.const fetchMoviesHandler = () => {
fetch("https://swapi.dev/api/films").then(response => {
return response.json();
}).then(data => {
data.results
});
});
function App() {
//응답 결과 data 상태에 저장
const [movies, setMovies] = useState();
const fetchMoviesHandler = () => {
fetch("https://swapi.dev/api/films/")
.then((response) => {
return response.json();
})
.then((data) => {
//파싱 후 추출된 영화 data를 movies 상태에 업데이트하기
setMovies(data.results);
});
};
return (
<React.Fragment>
<section>
<button onClick={fetchMoviesHandler}>Fetch Movies</button>
</section>
<section>
<MoviesList movies={movies} />
</section>
</React.Fragment>
);
};
그런데 이렇게 하면 오류가 뜬다. 왜냐하면 파싱 후 추출된 영화 data를 movies 상태에 업데이트 한 후, props으로 MoviesList 컴포넌트로 보내는데, 그 컴포넌트에서 사용하고 있는 이름이 다르기 때문이다.
//MoviesList 컴포넌트
const MovieList = (props) => {
return (
<ul className={classes["movies-list"]}>
{props.movies.map((movie) => (
<Movie
key={movie.id}
title={movie.title}
releaseDate={movie.releaseDate}
openingText={movie.openingText}
/>
))}
</ul>
);
};
따라서 이름을 똑같이 맞춰줘야 한다.
json 키 이름 변경해서 가져오기 위해 data.results를 map()하여 사용할 이름에 데이터를 매치시켜 객체를 각각 만들어 반환하는 새로운 배열을 만들고, 그 배열을 setMovies()로 상태에 넣어주자.
import React, { useState } from "react";
import MoviesList from "./components/MoviesList";
import "./App.css";
function App() {
//응답 결과 data 상태에 저장
const [movies, setMovies] = useState();
const fetchMoviesHandler = () => {
fetch("https://swapi.dev/api/films/")
.then((response) => {
return response.json();
})
.then((data) => {
//json 키 이름 변경해서 가져오기
const transformedMovies = data.results.map((movieData) => {
return {
id: movieData.episode_id,
title: movieData.title,
openingText: movieData.opening_crawl,
releaseDate: movieData.release_date,
};
});
//파싱 후 추출된 영화 data 트랜스폼한 후 movies 상태에 업데이트하기
setMovies(transformedMovies);
console.log(transformedMovies);
});
};
return (
<React.Fragment>
<section>
<button onClick={fetchMoviesHandler}>Fetch Movies</button>
</section>
<section>
<MoviesList movies={movies} />
</section>
</React.Fragment>
);
};
그럼 완성! 인줄 알았으나 또 오류가 겁나 뜬다.
데이터를 늦게 받아오면서 오류가 뜬 것이다 ㅠㅠ
MoviesList 컴포넌트에 props.movies가 있을 때만 데이터를 뿌려주게 하면, 데이터가 늦게 로딩되어도 에러가 뜨지 않는다.
//MoviesList 컴포넌트
const MovieList = (props) => {
return (
<ul className={classes["movies-list"]}>
{props.movies &&
props.movies.map((movie) => (
<Movie
key={movie.id}
title={movie.title}
releaseDate={movie.releaseDate}
openingText={movie.openingText}
/>
))}
</ul>
);
};
✅ 또는 movies 상태의 초기값으로 빈 배열을 보내줘도 된다.
const [movies, setMovies] = useState([]);
자, 이제 스타워즈 영화에 대한 데이터가 화면에 표시된다.
이는 스타워즈 API를 fetch해 온 것이다. 즉, 백엔드 앱에서 데이터베이스와 소통한 결과물이다.
이렇게 리액트 앱 내에서 백엔드로 HTTP 요청을 전송하여 데이터베이스에 있는 데이터들을 얻어올 수 있다.