본문 바로가기
JS/React 강의

[React] 22. HTTP 리퀘스트 보내기

by GiraffePark 2024. 1. 31.

안녕하세요. 박기린 입니다.

이번엔 React앱에서 외부 서버로부터 HTTP 요청을 보내고 결과값을 받아서 출력하는 방법에 대해 알아보겠습니다.

 

 


GET 요청 보내기

리액트의 근간은 JavaScript이기 때문에, fetchAPI를 통해서 서버에서 데이터를 주고받을 수 있습니다.

(fetchAPI는 JS 내장 기능이 아니라, WebAPIs 임을 잊지는 말자.)

 

fetch한 데이터를 state로 저장해서 이용할 수 있습니다.

그러면 데이터를 fetch할 때마다, state도 업데이트되면서 자동으로 컴포넌트도 업데이트 됩니다.

 

 

function App() {
  const [movies, setMovies] = useState([]);
  
  function fetchMoviesHandler() {
    fetch("https://swapi.dev/api/films")
      .then((response) => response.json())
      .then((data) => {
        const transformedMovies = data.results.map((movie) => {
          return {
            id: movie.episode_id,
            title: movie.title,
            openingText: movie.opening_crawl,
            releaseDate: movie.release_date,
          };
        });
        setMovies(transformedMovies);
      });
  }

  return (
    <React.Fragment>
      <section>
        <button onClick={fetchMoviesHandler}>Fetch Movies</button>
      </section>
      <section>
        <MoviesList movies={movies} />
      </section>
    </React.Fragment>
  );
}

export default App;

예제로 SWAPI라는 곳에서 스타워즈 영화 정보를 입력받아오는 코드를 작성했습니다.

SWAPU : https://swapi.dev/

 

 

 

위의 코드는 then으로 길게 연결되어 있어서 보기 힘들 수 있습니다. 바닐라 JS에서 then()의 훌륭한 대체제로 업데이트 된 async/await를 사용할 수 있습니다.

  async function fetchMoviesHandler() {
    const response = await fetch("https://swapi.dev/api/films");
    const data = await response.json();

    const transformedMovies = data.results.map((movie) => {
      return {
        id: movie.episode_id,
        title: movie.title,
        openingText: movie.opening_crawl,
        releaseDate: movie.release_date,
      };
    });
    setMovies(transformedMovies);
  }

 

 

 

위 코드를 실행하면, 사진과 같은 화면의 앱이 뜹니다.

 

 

Fetch Movies 버튼을 누르면,

SWAPI 서버로부터 전달받은 스타워즈 영화의 정보들을 출력합니다.

 

 

 

 

 

 

 


로딩 및 데이터 State 처리하기 (리액트에 로딩 구현하기)

리액트에서 로딩은 isLoading state를 만들어서 쉽게 구현할 수 있습니다.

 

 

import React, { useState } from "react";
import MoviesList from "./components/MoviesList";
import "./App.css";

function App() {
  const [movies, setMovies] = useState([]);
  const [isLoading, setIsLoading] = useState(false); // 로딩 중 여부 확인

  async function fetchMoviesHandler() {
    setIsLoading(true);
    const response = await fetch("https://swapi.dev/api/films");
    const data = await response.json();

    const transformedMovies = data.results.map((movie) => {
      return {
        id: movie.episode_id,
        title: movie.title,
        openingText: movie.opening_crawl,
        releaseDate: movie.release_date,
      };
    });
    setMovies(transformedMovies);
    setIsLoading(false);
  }

  return (
    <React.Fragment>
      <section>
        <button onClick={fetchMoviesHandler}>Fetch Movies</button>
      </section>
      <section>
        {!isLoading && movies.length > 0 && <MoviesList movies={movies} />}
        {!isLoading && movies.length === 0 && <p>Found no movies</p>}
        {isLoading && <p>Loading...</p>}
        
      </section>
    </React.Fragment>
  );
}

export default App;

 

  const [isLoading, setIsLoading] = useState(false); // 로딩 중 여부 확인
        {!isLoading && movies.length > 0 && <MoviesList movies={movies} />}
        {!isLoading && movies.length === 0 && <p>Found no movies</p>}
        {isLoading && <p>Loading...</p>}

isLoading state를 선언하고, bool 값에 따라 Loading... 메세지를 띄우거나 데이터를 출력하도록 지정합니다.

또, movies state에 아무런 요소가 없는 경우 Found no movies 메세지를 띄우도록 설정합니다.

 

 

 

 

처음으로 페이지를 접속했을 때는 movies state에 아무 데이터가 없는 상태입니다.

그래서 Found no movies 문구가 뜹니다.

 

 

 

 

버튼을 눌러서 로딩 중일 때,

 

 

 

 

서버로부터 데이터를 받은 후, setIsLoading(false)가 동작하면서, 로딩 문구가 사라지고 movies 데이터가 출력됩니다.

 

 

 

 

 

 


http 오류 처리하기

// const response = await fetch("https://swapi.dev/api/films"); <- 원래 URL

const response = await fetch("https://swapi.dev/api/film"); // films 뒤의 s 제거

일부로 url을 틀리게 적어봅니다.

 

 

 

 

그러면 404에러가 발생합니다. 서버가 응답을 받았지만, 잘못된 요청이기에 에러가 발생했습니다.

그런데 홈페이지는 여전히 'Loading...'문구만 표시하고 있습니다.

서버와 연결하는 과정에서 다양한 에러가 발생할 수 있고, 에러에 대해 적절한 대응방안이 있어야 합니다.

 

 

 

에러를 대처하기 위해서, 리액트에서는 error state를 이용할 수 있습니다.

const [error, setError] = useState(null);

setError(null);

error state의 초기값은 null로 두고, 비동기 함수가 시작될 때마다 setError(null)로 초기화를 해서, 이전에 받았을 수도 있는 오류를 초기화합니다.

 

 

비동기 함수는 .then()과 async/await 두 가지로 나눠집니다.

.then() 구문에서는 .catch() , async/await 구문에서는 try-catch 구문을 이용해서 error를 잡아낼 수 있습니다.

문제는, fetchAPI는 error status code (ex: 404 error)를 실제 에러로 취급하지 않습니다. 404 코드와 같은 오류 상태 코드(error status code)를 전달받아도, 기술적인 오류로 처리하지 않아서 프로그램이 종료되지 않습니다. 그래서 'Loading...'구문이 계속 앱 상에서 출력되는 거기도 합니다.

오류로 인해 제대로 가져오지 못한 데이터로, JS의 함수를 이용해서 추가적인 작업을 하려고 할 때 발생하는 TypeError만 오류로 취급하고 프로그램이 종료됩니다.

 

 

그래서 error status code를 받자마자 실제 오류가 뜨게끔 직접 지정해줄 필요가 있습니다. Axios라는 서드 파티 라이브러리를 사용하면 쉽게 이걸 구현할 수 있지만, 라이브러리에 의존하지 않고 직접 만들 수 있습니다.

 

방법은 promise 객체의 'ok' property를 이용하는 것입니다. 에러 없이 데이터를 가져온다면 true, 반대로 에러가 있다면 false를 반환합니다. 만약 false일 경우 Error를 throw하는 조건문을 작성해서 catch()가 에러를 처리할 수 있도록 만들어줍니다.

 

 

 

  async function fetchMoviesHandler() {
    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((movie) => {
        return {
          id: movie.episode_id,
          title: movie.title,
          openingText: movie.opening_crawl,
          releaseDate: movie.release_date,
        };
      });
      setMovies(transformedMovies);
    } catch (error) {
      setError(error.message);
    }
    setIsLoading(false);
  }

 

      if (!response.ok) {
        throw new Error("Something went wrong!");
      }

위의 조건문을 달아줍니다.

 

    } catch (error) {
      setError(error.message);
    }

그리고 이 에러를 전달받으면, catch 구문에서 setError()를 실행해줍니다.

 

 

 

 

let content = <p>Found no movies.</p>;

  if (movies.length > 0) {
    content = <MoviesList movies={movies} />;
  }

  if (error) {
    content = <p>{error}</p>;
  }

  if (isLoading) {
    content = <p>Loading...</p>;
  }

  return (
    <React.Fragment>
      <section>
        <button onClick={fetchMoviesHandler}>Fetch Movies</button>
      </section>
      <section>{content}</section>
    </React.Fragment>
  );

여기에서 그치지 않고, error state에 변화가 생기면 App 상에 출력이 되도록 위와 같은 코드를 작성합니다.

 

 

 

 


useEffect()를 사용해서 요청 보내기

우선 useEffect(), useCallback() 설명글을 보고 오시는 것을 추천드립니다.

useEffect() 설명글 : https://arnopark.tistory.com/770

useCallback() 설명글 : https://arnopark.tistory.com/845

useEffect와 useCallback의 의존성 배열에 대한 설명글 : https://arnopark.tistory.com/846

 

 

 

 

useEffect를 이용해서, 특점 컴포넌트가 로딩되자마자 (ex: 페이지가 처음 로딩되자마자) http 요청을 바로 전송할 수 있게 지정할 수 있습니다. (ComponentDidMount에 http 리퀘스트가 전송되는 방식입니다.)

 

 

  useEffect(() => {
    fetchMoviesHandler();
  }, []); // 의존성 배열이 비어있으면, 컴포넌트 함수가 최초로 로딩될 때 빼고는 절대 재실행되지 않는다.

하지만 이 방식은 썩 좋지 않아보입니다.

useEffect의 적절한 사용법은, 모든 의존성 요소들을 의존성 배열에 추가해줘야 하기 때문입니다.

 

 

 

  useEffect(() => {
    fetchMoviesHandler();
  }, [fetchMoviesHandler]);

위 코드처럼 말이죠.

하지만 state나 props의 변경으로 컴포넌트 함수가 재실행/재평가된다면, 'fetchMoviesHandler'함수 역시 재실행/재평가가 되기 때문에, 이에 따라 useEffect()도 쓸데없는 재실행이 일어날 것입니다. 

 

 

 

이를 방지하기 위한 방법으로 useCallback()을 사용하면 됩니다. useCallback()에 fetchMoviesHandler()를 추가한 다음, useEffect의 의존성으로 추가합니다.

  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((movie) => {
          return {
            id: movie.episode_id,
            title: movie.title,
            openingText: movie.opening_crawl,
            releaseDate: movie.release_date,
          };
        });
        setMovies(transformedMovies);
      } catch (error) {
        setError(error.message);
      }
      setIsLoading(false);
    }
  }, []);

  useEffect(() => {
    fetchMoviesHandler();
  }, [fetchMoviesHandler]);

이렇게 하면 컴포넌트가 재실행되어도 fetchMoviesHandler는 재생성되지 않을 것이고, useEffect 또한 재실행되지 않을 것입니다. 자원 낭비 없이 온전하게 기능을 수행할 수 있습니다.

 

 

 

이제는 FetchMovies 버튼을 누르지 않아도 앱을 실행하자마자 영화정보들이 출력됩니다.

 

 

 

주의사항!

자바스크립트의 hoisting 문제 때문에, useEffect 구문은 'useCallback 적용된 fetchMoviesHandler함수 코드'보다 밑에 있어야 정상적으로 작동됩니다.

 

 

그렇지 않으면 위와 같은 에러가 나타납니다.

 

 

 

반응형