HTTP 요청 보내기
HTTP 요청의 기본 개념
- 클라이언트에 직접적으로 서버를 연결하면 자바스크립트 코드는 누구나 확인할 수 있기 때문에 인증 정보를 노출시키는 행위이며 보안에 취약해집니다.
- HTTP 요청에 대한 API를 말할 때는 보통 REST 또는 GraphQL API를 말하며 이 두 개는 서버가 데이터를 노출하는 방식에 대한 서로 다른 표준입니다.
- Fetch API는 브라우저 내장형이며 데이터를 불러오고 이름과는 다르게 전송도 가능합니다.
영화에 대한 정보를 불러와 그를 보여주는 간단한 코드
사용한 API 주소: https://swapi.dev/
SWAPI - The Star Wars API
What is this? The Star Wars API, or "swapi" (Swah-pee) is the world's first quantified and programmatically-accessible data source for all the data from the Star Wars canon universe! We've taken all the rich contextual stuff from the universe and formatted
swapi.dev
const [movies, setMovies] = useState([]);
// 이제 이 함수가 호출될 때 마다 매 번 HTTP 요청이 전송됨
function fetchMoviesHandler () {
// fetch를 통해 프로미스 객체를 반환함
fetch('https://swapi.dev/api/films/')
// 왜? then을 사용해야 하냐? - HTTP 요청은 즉각 끝나는 작업이 아니기에 미래의 어느 시점에서 확인할 수 있음
// 이 말은 즉, 끝나는 시점에 맞춰 then을 이용하여 비동기 처리를 해야함
// 요청 받은 데이터는 JSON 형식이므로 코드에서 사용할 수 있는 객체로 반환함
.then(response => response.json())
// 그 변환된 데이터를 useState로 movies에 넣어줌
.then((data) => {
setMovies(data.results);
}
}
위처럼 데이터를 바로 넣어줄 수 있겠지만 실제로 짜인 코드와 불러오는 데이터에서 키 값에 차이가 있을 수 있다.
그러므로 map을 이용하여 원래 짜인 키 값에 맞춰 적절히 가공해 줄 수 있다.
물론, 받아오는 데이터의 키 값에 맞게 코드를 수정하는 방법도 있다.
const [movies, setMovies] = useState([]);
function fetchMoviesHandler () {
fetch('https://swapi.dev/api/films/')
.then(response => response.json())
.then((data) => {
const transformedMovies = data.results.map(movieData => {
return {
id: movieData.episode_id,
title: movieData.title,
openingText: movieData.opening_crawl,
releaseDate: movieData.release_date
};
});
setMovies(transformedMovies);
}
}
위 설명의 이해를 돕기 위한 더미데이터와 API 요청에 의해 가져온 데이터 비교
async, await
- 위의 fetch를 이용한 방법과 똑같이 async, await를 이용하여 코드를 간결하게 할 수 있다.
- 이건 리액트의 특별 기능이 아닌 자바스크립트의 기본 기능이다.
- 실제로 이 코드가 실행될 땐 await는 then으로 변역 되어 실행된다.
const [movies, setMovies] = useState([]);
async function fetchMoviesHandler () {
const response = await fetch('https://swapi.dev/api/films/')
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);
}
로딩 화면과 빈 데이터 구현하기
프로젝트를 진행하면서 멘토님이 프론트엔드에서 핵심적인 역할은 빈 화면을 노출시키지 않는 것이라고 했습니다.
데이터가 없거나 로딩 중 일 경우에도 무언가를 화면에 보여주어야 합니다.
const [movies, setMovies] = useState([]);
const [isLoading, setIsLoading] = useState(false);
async function fetchMoviesHandler () {
// 아래의 코드들이 실현되기 전에 로딩 상태를 true로 만듬
setIsLoading(true);
const response = await fetch('https://swapi.dev/api/films/')
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);
// 모든 코드가 돌아갔다면 다시 로딩 상태를 false로 만듬
setIsLoading(false);
}
const App () => {
return (
<React.Fragment>
<!--로딩은 되었고 보여줄 내용이 있는 경우-->
{!isLoading && movies.length > 0 && <MovieList/>}
<!--로딩은 되었지만 보여줄 내용이 없는 경우-->
{!isLoading && movies.length === 0 && <EmptyList/>}
<!--로딩이 안 된 경우-->
{isLoading && <LoadingPage/>}
</React.Fragment>
)
}
오류 관리
- 서버와 통신하는 데 있어 다양한 오류들이 존재하고 다양한 상황에 맞게 설정된 응답 코드들이 존재합니다.
- 대표적으로 2xx로 시작하는 응답 코드는 정상적인 응답을 뜻합니다. 즉 요청이 전송되었고 서버가 성공적으로 반응한 것입니다.
- 서버가 401 또는 403과 같은 응답을 한다면 서버가 요청을 받았으나 원하는 응답을 주지 않았음을 의미합니다. 이는 기술적으로 성공적으로 응답을 받았으나 응답에 오류 상태 코드가 포함되어 있어서 오류 문자를 받는 것을 의미합니다.
- 5xx 응답들은 서버에 오류가 있을 때 발생합니다.
이렇게 다양한 상황에 맞는 응답 코드들이 존재하며 그에 관한 자세한 내용은 아래 링크에서 확인할 수 있습니다.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
로딩과 빈 데이터 처리와 비슷하게 오류에 관한 화면 설정도 이루어져야 합니다.
모두 나쁜 사용자 경험(UX)과 연결될 수 있기 때문입니다.
const [movies, setMovies] = useState([]);
const [isLoading, setIsLoading] = useState(false);
// 오류 관리를 위한 새로운 state 설정
// 초기에는 오류가 없기 때문에 null
const [error, setError] = useState(null);
async function fetchMoviesHandler () {
setIsLoading(true);
// 이전에 받았을 수 있는 오류 초기화
setError(null);
try{
const response = await fetch('https://swapi.dev/api/films/')
// 응답이 잘못 되었을 때에 대한 오류 메시지 설정
// json으로 파싱하기 전에 오류를 먼저 체크
// axios는 자동으로 설정해주기도 한다...
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) {
setError(error.message);
}
// 성공적인 응답이나 오류를 받았든 간에 상관없이 로딩 끝내기
setIsLoading(false);
}
const App () => {
return (
<React.Fragment>
{!isLoading && movies.length > 0 && <MovieList/>}
<!--로딩이 끝났지만 데이터도 없고 에러도 아닌 경우-->
{!isLoading && movies.length === 0 && !error && <EmptyList/>}
<!--로딩이 끝났는데 에러가 있는 경우-->
{!isLoading && error && <p>{error}</p>}
{isLoading && <LoadingPage/>}
</React.Fragment>
)
}
요청에 useEffect 사용하기
- 컴포넌트가 로드될 때 즉각적으로 HTTP 요청을 보내기 위해 useEffect를 사용합니다.
- useCallback 훅을 이용하여 함수가 불필요하게 재생성되는 것을 막아 무한 루프를 방지했습니다.
- useEffect의 의존성 배열을 비우는 방법도 있으나 최적화는 아닙니다.
const [movies, setMovies] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const fetchMoviesHandler = useCallback(async () => {
setIsLoading(true);
setError(null);
try{
const response = await fetch('https://swapi.dev/api/films/')
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) {
setError(error.message);
}
setIsLoading(false);
}, []);
// 함수를 의존성 목록에 올림
useEffect(() => {
fetchMoviesHandler();
}, [fetchMoviesHandler]);
POST
async function addMovieHandler(movie) {
const response = await fetch(URL, {
method: 'POST',
body: JSON.stringify(movie),
headers: {
// 필요 없는 경우도 있지만 대다수의 api는 헤더를 필요로하며
// 이 헤더를 통해 어떤 컨텐츠가 전달되는지 알 수 있음
'Content-Type': 'application/json'
}
});
const data = await response.json();
}