Home 07-http-요청과-커스텀-훅
Post
Cancel

07-http-요청과-커스텀-훅

강의 링크


1. 요청 보내고 응답 처리 하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import React, { useState, useEffect, useCallback } from 'react';

import MoviesList from './components/MoviesList';
import './App.css';

function App() {
  const [movies, setMovies] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  async function fetchMoviesHandler() => {
    // 초기화
    setIsLoading(true);
    setError(null);
    
    try {
      // 데이터 fetch
      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,
        };
      });
      
      // 데이터를 가져오면 transformedMovies에 저장하고 loading은 끝난다. 
      setMovies(transformedMovies);
      setIsLoading(false);
    } 
      // 데이터를 못가져오면 에러 메시지를 저장한다. 
      catch (error) {
      setError(error.message);
    }
  });

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

  // 정상적으로 데이터를 fetch 해왔을 때
  if (movies.length > 0) {
    content = <MoviesList movies={movies} />;
  }

 // error가 발생했을 때
  if (error) {
    content = <p>{error}</p>;
  }
    
// 로딩중 일 때
  if (isLoading) {
    content = <p>Loading...</p>;
  }

  return (
    <React.Fragment>
      <section>
         <!--버튼을 누르면 데이터를 fetch 한다.  --> 
        <button onClick={fetchMoviesHandler}>Fetch Movies</button>
      </section>
      <section>{content}</section>
    </React.Fragment>
  );
}

export default App;


2. useEffect() 활용하기

특정 컴포넌트가 로딩되자마자 데이터를 가져오기도 한다.

이 때 사용할 수 있는게 useEffect()이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import React, { useState, useEffect, useCallback } from 'react';

import MoviesList from './components/MoviesList';
import './App.css';

function App() {
  const [movies, setMovies] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  // useCallback을 사용하는 이유는 참조 동일성 때문이다. 
  // 함수가 변경되면 useEffect가 재실행되기 때문에 useCallback을 사용하지 않으면 무한루프의 위험이 있다. 
  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);
  }, []);

  // 함수가 의존성 배열에 들어가있으므로, 함수는 useCallback을 사용해야 한다. 
  useEffect(() => {
    fetchMoviesHandler();
  }, [fetchMoviesHandler]);

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

export default App;
  • HTTP 요청 전송은 일종의 사이드 이펙트로 컴포넌트의 상태를 바꿔버리기 때문에 useEffect()를 사용할 수 있다.


3. 커스텀 훅 제작하기

1) 커스텀 훅이란

커스텀 훅이란 상태를 설정할 수 있는 로직을 포함한 함수이다.

커스텀 훅을 만들어서, 재 사용 가능한 함수에 상태를 설정하는 로직을 아웃소싱할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/hooks/use-counter.js
// 위의 예시는 props로 넘기는 값에 따라 +를 하는 카운터를 만들거나 -를 하는 카운터를 만든다.

import { useState, useEffect } from 'react';

// 함수 인자를 통해 재활용성을 높일 수 있다. 
const useCounter = (forwards = true) => {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      if (forwards) {
        setCounter((prevCounter) => prevCounter + 1);
      } else {
        setCounter((prevCounter) => prevCounter - 1);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [forwards]);

  return counter;
};

export default useCounter;
  • 커스텀 훅을 통해, 다른 컴포넌트에서 사용할 수 있는 로직을 커스텀 훅으로 아웃 소싱 할 수 있다.
  • 함수 이름은 use로 시작해야 한다.
    • 리액트에게 이 함수가 커스텀 훅임을 알려주며 이는 리액트가 해당 함수를 훅의 규칙에 따라 사용하겠다고 보장하는 것이다.


2) useHttp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/hooks/use-http.js

import { useState, useCallback } from 'react';

const useHttp = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  // useCallback을 사용하는 이유는 다음과 같다.
  // 훅을 사용할 때, useEffect로 해당 함수를 의존성 배열에 추가하게 되는데 참조 동일성을 지켜 무한 루프에 빠지지 않도록 한다. 
  const sendRequest = useCallback(async (requestConfig, applyData) => {
    setIsLoading(true);
    setError(null);
    try {
      const response = await fetch(requestConfig.url, {
        method: requestConfig.method ? requestConfig.method : 'GET',
        headers: requestConfig.headers ? requestConfig.headers : {},
        body: requestConfig.body ? JSON.stringify(requestConfig.body) : null,
      });

      if (!response.ok) {
        throw new Error('Request failed!');
      }

      const data = await response.json();
      applyData(data);
    } catch (err) {
      setError(err.message || 'Something went wrong!');
    }
    setIsLoading(false);
  }, []);

  return {
    isLoading,
    error,
    sendRequest,
  };
};

export default useHttp;


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// src/App.js

import React, { useEffect, useState } from 'react';

import Tasks from './components/Tasks/Tasks';
import NewTask from './components/NewTask/NewTask';
import useHttp from './hooks/use-http';

function App() {
  const [tasks, setTasks] = useState([]);

  const { isLoading, error, sendRequest: fetchTasks } = useHttp();

  useEffect(() => {
    const transformTasks = (tasksObj) => {
      const loadedTasks = [];

      for (const taskKey in tasksObj) {
        loadedTasks.push({ id: taskKey, text: tasksObj[taskKey].text });
      }

      setTasks(loadedTasks);
    };

    // 훅에서 정의된 함수들은 훅이 사용되는 곳에서 호출할 수 있다. ``
    // 즉, 훅을 사용하는 컴포넌트에서 호출할 수 있다. 
    fetchTasks(
      { url: 'https://react-http-6b4a6.firebaseio.com/tasks.json' },
      transformTasks
    );
  }, [fetchTasks]);

  const taskAddHandler = (task) => {
    setTasks((prevTasks) => prevTasks.concat(task));
  };

  return (
    <React.Fragment>
      <NewTask onAddTask={taskAddHandler} />
      <Tasks
        items={tasks}
        loading={isLoading}
        error={error}
        onFetch={fetchTasks}
      />
    </React.Fragment>
  );
}

export default App;
  • 컴포넌트가 생성되면서 useHttp함수를 호출되고 useEffect가 실행된다.
  • 그 결과, isLoading, error, sendRequest(fetchTask)가 반환된다.
  • useEffect가 실행되면서 sendRequest(fetchTask)함수가 실행된다.
    • 데이터를 fetch하고 데이터를 state에 저장하는 함수
  • 데이터가 state에 저장되면 state가 변경되어 컴포넌트가 리랜더링된다.


This post is licensed under CC BY 4.0 by the author.