리액트 앱과 어떠한 종류의 데이터베이스가 있을 때 이 둘을 직접적으로 연결시켜서는 절대 안된다! 직접적으로 연결할 경우 데이터베이스의 인증 정보가 노출될 가능성이 높기 때문이다. 따라서 데이터베이스 자체와는 백앤드 앱이 통신을 하고, 리액트 앱에서는 백엔드 앱 (백엔드 API와 같은 것)과 통신하여 작동하게된다.
API는 리액트나 HTTP 요청에만 국한되어 사용되는 개념은 아니고 훨씬 더 넓은 개념과 사용범위를 가진다. API에는 어떠한 결과를 얻기 위한 작업이 규칙으로 명확하게 명시되어 있다. 우리는 이러한 API를 통해서 명확하게 정의된 인터페이스와 통신하게된다. 결국 프로그램들이 상호작용하기 위한 인터페이스를 의미한다.
HTTP 요청과 관련하여 API를 말할 때는 보통 REST API나 GraphQL API를 말한다. 이들은 서버가 데이터를 어떻게 노출할 것인지에 대한 서로 다른 표준이다. 이번 섹션에서는 REST API를 통해 포스팅을 해보려 한다.
✅ REST
REST는 자원, 행위, 메시지 3가지로 이루어져 있다.
자원: 접근할 대상 (URL)
행위: HTTP의 4가지 메소드 GET(조회), POST(생성), PUT(수정), DELETE(삭제)
메시지: 주고받는 데이터의 정보 ({"name": "서진", "like": "cosmos"})
✅ REST API
REST API는 REST 아키텍처를 따르는 API로 서로 다른 URL에 각각 다른 요청을 보내게 되면 특정한 형식에 맞춰서 그에 맞는 서로 다른 데이터를 제공해주는 것이다.
ps) Star Wars API
이번 실습을 하면서 알게된 웹사이트인데, 스타워즈 영화와 관련된 다양한 데이터를 제공하는 API이다. 백엔드와 통신하는 부분을 공부할 때 사용하면 좋을 듯 하다!
AXIOS
어떤 자바스크립트 라이브러리를 사용하는가에 관계 없이 HTTP 요청 전송을 하고 이에 대한 반응을 매우 간단하게 확인할 수 있는 패키지이다.
Fetch API
HTTP 요청을 전송하는 자바스크립트의 내장 매커니즘이다. Fetch API는 브라우저에도 내장되어있다. 이를 통해 데이터를 전송하고 받아오는 작업을 할 수 있다. 이번 포스팅은 Fetch API를 사용해서 진행해볼 예정이다.
Fetch API를 사용함으로써 브라우저에서 사용할 수 있는 함수
fetch('URL주소', {})
HTTP request를 보내는 기본적인 fetch 함수이다.
fetch('https://velog.io/@cosmos7', { method: 'POST', body: JSON.stringify({ user: "seojin", like: "cosmos", }), });
✅ 첫번째 인자
요청을 전송하려는 URL를 string으로 넣어주면 된다.✅ 두번째 인자
다양한 선택사항을 객체 형태로 전달할 수 있다. 예를 들면 추가적인 바디나 헤더를 만들거나 HTTP 요청 메소드를 변경하는 등의 작업을 할 수 있다. 이때 default 요청 메소드는 'GET'이다. 생략도 가능하다.✅ return 값
promise 객체를 반환한다. promise 객체는 잠재적으로 발생할 수 있는 오류나 호출에 대한 응답에 반응할 수 있게 해준다. 또한, 즉각적으로 무언가 행동을 하는 함수가 아닌 비동기적으로 어떤 데이터를 전달하는 객체이다.
HTTP 요청을 전송하는 것은 비동기 방식이다. 시행했다고해서 즉각적으로 끝나는 작업이 아니기 때문에 Promise 객체를 잘 활용해야 한다. fetch를 한 후 바로 다음줄에서 작업을 바로 시작하는 코드를 짜는 것이 아닌,
.then
을 활용하여 코드를 작성해야한다..then()
내부에서는 받아온 응답을 활용할 수 있으므로 내부에 화살표 함수를 선언해서 받아온 응답을 사용하여 코드를 작성할 수 있다.이때 받아온 response는 JSON 형식의 객체이며 요청의 응답에 대한 많은 데이터를 가지고 있다. response는 데이터 내용 자체 뿐만 아니라 응답 헤더를 읽거나 브라우저 상태 코드를 얻을 수 있다.
서버에 보낸 request에 대한 response로 받아온 API 데이터는 JSON 형식으로 받아와진다. JSON은 데이터 교환에 사용하는 형식 중 하나이다. 형태는 자바스크립트의 객체와 비슷하게 key: vlaue의 형태로 이루어져 있다. 하지만 JSON 데이터 내부에는 데이터 값들만 들어있고 메소드는 들어있지 않다.
.json() 메소드
JSON 데이터의 장점은 자바스크립트 객체로 변환하기가 아주 쉽다는 점이다. response 객체에는
json()
내장 메소드가 있어서 JSON response의 본문을 자바스크립트 코드에서 사용할 수 있는 객체 형태로 자동 변환해준다.json()
메소드 또한 promise 객체를 사용하므로 chain 형태로.then()
을 사용할 수 있다. promise를 사용하여 받아온 json 형식의 데이터가 자바스크립트 객체 형태로 모두 변환된 후에 다음 작업을 할 수 있도록 한다. 또한.catch()
를 통해 잠재적으로 발생할 수 있는 오류를 처리할 수 있다.
이번 섹션에서는 이렇게 변환된 JSON 데이터를 state에 저장하여 다른 component들에 prop형태로 전달하여 사용하려고 한다. 이 경우에는 prop에서 사용하는 이름과 API에 지정되어 있는 이름이 다르니, response로 받아온 데이터를 prop에서 사용하는 이름으로 변환하는 작업이 필요하다. 혹은 prop에서 쓰는 이름을 API에서 지정된 이름으로 변환하는 방법도 있다.
// StarWars API 사용!
import React, { useState } from 'react';
function App() {
const [movies, setMovies] = useState([]);
function fetchMoviesHandler() {
fetch('https://swapi.dev/api/films/')
// json 메소드를 통해 JS 객체로 변환
.then((response) => {
return response.json();
})
// 변환한 값 data로 받아서 사용
.then((data) => {
const transformedMovies = data.results.map((movieData) => {
// props 이름에 맞게 API값 변경
return {
id: movieData.episode_id,
title: movieData.title,
openingText: movieData.opening_crawl,
releaseDate: movieData.release_date,
};
});
// state에 저장
setMovies(transformedMovies);
});
}
return (...생략)
}
export default App;
위에서 쓴 것과 같이 .then
chain을 만드는 것과 다른 방식으로 async-await를 사용하여 코드를 작성하면 더 깔끔하다. 내부적으로 동작하는 방식은 .then을 쓴 것과 똑같다!
함수 앞에 async
예약어를 추가하고, promise를 반환하는 작업 앞에 await
예약어를 사용한다.
// 함수 앞에 async 예약어
async function fetchMoviesHandler() {
// promise를 반환하는 fetch에 await 예약어
const response = await fetch('https://swapi.dev/api/films/')
// await해서 받아온 response에 json메소드 적용
const data = await response.json();
// await해서 변환된 response에 json메소드 적용
const transformedMovies = data.results.map((movieData) => {
return {
id: movieData.episode_id,
title: movieData.title,
openingText: movieData.opening_crawl,
releaseDate: movieData.release_date,
};
});
setMovies(transformedMovies);
}
HTTP 상태 코드
HTTP 응답 상태 코드는 특정 HTTP 요청이 성공적으로 완료되었는지 알려준다. 대표적으로 쓰이는 것들은 다음과 같다.
200번대 응답코드
정상적인 응답을 뜻한다. 요청이 전송되었고 서버가 성공적으로 반응한 것이다.400번대 에러코드
요청이 성공적으로 전송되었지만 서버에서 원하는 응답을 주지 않았음을 의미한다.500번대 에러코드
서버에 오류가 있다.
이러한 HTTP 상태 코드를 사용해서 에러를 처리하기 위해서는 state를 사용하면 된다. 만약 .then
을 사용하여 작업한다면 뒤에 .catch
를 추가해서 오류를 확인해야 한다. async-await
를 사용한다면 try-catch
를 사용해서 작성해야 한다.
AXIOS
는 오류 상태 코드에 맞는 오류를 만들어서 전달하는 반면 fetch API
는 에러 상태코드를 실제 에러로 취급하지 않는다. 따라서fetch API
를 사용할 경우에는 에러 헨들링을 직접 만들어서 해야 한다.
✅ response 객체에는 요청이 성공적이었는지 아닌지를 확인하는
ok필드
가 있다. 이것 외에도 어떠한 오류 상태 코드를 가지는지 반환해주는status 필드
도 있다.
import React, { useState } from 'react';
function App() {
const [movies, setMovies] = useState([]);
const [error, setError] = useState(null);
async function fetchMoviesHandler() {
try {
const response = await fetch('https://swapi.dev/api/films/');
// 만약 여기서 요청이 성공적이지 않다면 error 발생시키고 try 나가서 catch문으로 이동
if (!response.ok) {
throw new Error('Something went wrong!');
}
// 아래 코드가 실행되지 않고
const data = await response.json();
const transformedMovies = data.results.map((movieData) => {
return {
id: movieData.episode_id,
title: movieData.title,
openingText: movieData.opening_crawl,
releaseDate: movieData.release_date,
};
});
setMovies(transformedMovies);
// catch문으로 이동하게된다
// 여기서 받아오는 error는 state값이 아닌 new Error로 만든 객체값
} catch (error) {
setError(error.message);
}
}
return (...생략);
}
export default App;
위의 예시는 이해를 돕기 위한 코드이고, 보통은 컴포넌트가 로딩되자마자 데이터를 가져오는 경우가 대부분이다.
위에서 작성한 fetchMoviesHandler()
를 useEffect()
안에 넣어서 dependencies 없이 사용하면 컴포넌트가 최초로 렌더링될 때 한 번 데이터를 받아오고 그 후로는 받아오는 작업을 하지 않는다. 버튼에 onClick으로 연결을 해두면 버튼을 누를 경우 다시 데이터를 받아올 수 있도록 구현 가능하다.
useEffect(() => {
fetchMoviesHandler();
}, [])
하지만 useEffect
의 dependencies에는 해당 useEffect 내부에서 사용하는 모든 것을 dependencies로 넣어주는게 좋은 코드이므로,
useEffect(() => {
fetchMoviesHandler();
}, [fetchMoviesHandler])
로 작성하여 fetchMoviesHandler
내부의 어떠한 상태값 등으로 인해 함수가 변경된다면 재실행하여 데이터를 가져올 수 있도록 한다.
하지만 함수는 객체이고, 자바스크립트 특성상 컴포넌트가 re-rendering 될 때마다 함수가 다시 작성된다는 것을 지난 섹션에서 다루었다. 따라서 해당 함수를 dependencies로 추가하게되면 무한루프가 발생하게 된다.
이를 해결하기 위해선 useCallback
을 사용하여 fetchMoviesHandler
를 감싸야 한다.
useEffect(() => {
fetchMoviesHandler();
}, [fetchMoviesHandler])
const fetchMoviesHandler = useCallback(async () => {
... 생략
}, []);
위에서 사용한 starwars API
는 말 그대로 dummy 데이터를 제공하는 API이므로 POST 요청을 보낼 수 없었다. 따라서 이제부터는 Firebase API를 사용할 것이다. 이것은 구글에서 제공하는 서비스로 코드 작성 없이도 사용이 가능한 백엔드이다. 완전한 REST API를 연습 가능하도록 제공하기 때문에 POST 요청에 대해서도 다뤄볼 수 있다.
이때까지는 GET 요청을 통해 서버에서 데이터를 받아오기만 했다. 하지만 클라이언트단에서 입력한 값을 서버에 저장하거나 업데이트 하는 등의 작업도 분명 필요하다. 그러한 작업을 하기 위해서 POST 요청을 전송한다.
fetch()
를 이용하여 POST 요청을 보내기 위해서는 두번째 인자에 PSOT 메소드를 넣어주면 된다.
POST 요청을 했을 때 서버에서 하는 작업은 API마다 다르지만, 지금 사용하는
firebase
에서는 POST 요청을 보내면 리소스를 만들어둔다. 따라서 요청을 보낼 때 body에 리소스를 같이 만들어서 보내주면 해당 리소스를 firebase가 저장해준다.body
에 들어가는 코드는 자바스크립트 객체가 아닌 JSON 데이터이어야 한다. 자바스크립트 객체를 JSON으로 바꾸기 위해서는 브라우저에 내장되어JSON.stringify
를 사용하면 된다.header
firebase 자체에서는 헤더 정보가 전달되지 않아도 아무런 에러 없이 동작하지만, 대다수의 API들을 이러한 헤더 정보를 필요로 한다. 내부에 어떠한 컨텐츠가 전달되는지에 대한 정보인Content-Type
등을 객체 형태로 저장해서 전달한다.
async function addMovieHandler(movie) {
const response = await fetch('firebaseURL주소', {
method: 'POST',
body: JSON.stringify(movie),
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
}