데이터를 가져오고 캐시하기 위해 사용자 지정 반응 후크를 만드는 방법

게시 됨: 2022-03-10
빠른 요약 ↬ React 애플리케이션의 많은 구성 요소가 사용자에게 표시될 데이터를 검색하기 위해 API를 호출해야 할 가능성이 높습니다. componentDidMount() 수명 주기 메서드를 사용하여 이미 가능하지만 Hooks의 도입으로 데이터를 가져오고 캐시하는 사용자 지정 후크를 빌드할 수 있습니다. 이것이 이 튜토리얼에서 다룰 내용입니다.

React Hooks를 처음 접하는 경우 공식 문서를 확인하여 이해하는 것으로 시작할 수 있습니다. 그런 다음 Shedrack Akintayo의 "Getting Started With React Hooks API"를 추천합니다. 당신이 따라갈 수 있도록 Adeneye David Abiodun이 작성한 React Hooks의 모범 사례를 다루는 기사도 있습니다. 이 기사는 당신에게 유용할 것이라고 확신합니다.

이 기사 전체에서 Hacker News Search API를 사용하여 데이터를 가져오는 데 사용할 수 있는 사용자 지정 후크를 빌드합니다. 이 튜토리얼은 Hacker News Search API를 다루지만 우리가 전달하는 유효한 API 링크에서 응답을 반환하는 방식으로 후크 작업을 할 것입니다.

모범 반응 사례

React는 풍부한 사용자 인터페이스를 구축하기 위한 환상적인 JavaScript 라이브러리입니다. 인터페이스를 잘 작동하는 코드로 구성하기 위한 훌륭한 구성 요소 추상화를 제공하며 사용할 수 있는 거의 모든 것이 있습니다. React 관련 기사 읽기 →

React 컴포넌트에서 데이터 가져오기

React hooks 이전에는 componentDidMount() 수명 주기 메서드에서 초기 데이터를 가져오고 componentDidMount() componentDidUpdate() 주기 메서드에서 prop 또는 state 변경을 기반으로 데이터를 가져오는 것이 일반적이었습니다.

작동 방식은 다음과 같습니다.

 componentDidMount() { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=JavaScript` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } componentDidUpdate(previousProps, previousState) { if (previousState.query !== this.state.query) { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${this.state.query}` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } }

componentDidMount 라이프사이클 메소드는 컴포넌트가 마운트되는 즉시 호출되며, 완료되면 Hacker News API를 통해 "JavaScript"를 검색하고 응답을 기반으로 상태를 업데이트하도록 요청했습니다.

반면 componentDidUpdate 수명 주기 메서드는 구성 요소에 변경 사항이 있을 때 호출됩니다. 상태의 "데이터"를 설정할 때마다 메서드가 호출되는 것을 방지하기 위해 상태의 이전 쿼리를 현재 쿼리와 비교했습니다. 후크를 사용하여 얻을 수 있는 한 가지는 두 가지 수명 주기 메서드를 더 깔끔한 방식으로 결합하는 것입니다. 즉, 구성 요소가 마운트될 때와 업데이트할 때 두 가지 수명 주기 메서드가 필요하지 않습니다.

점프 후 더! 아래에서 계속 읽기 ↓

useEffect Hook으로 데이터 가져오기

useEffect 후크는 구성 요소가 마운트되자마자 호출됩니다. 일부 소품 또는 상태 변경을 기반으로 후크를 다시 실행해야 하는 경우 종속성 배열( useEffect 후크의 두 번째 인수)에 이를 전달해야 합니다.

후크로 데이터를 가져오는 방법을 살펴보겠습니다.

 import { useState, useEffect } from 'react'; const [status, setStatus] = useState('idle'); const [query, setQuery] = useState(''); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]);

위의 예에서 queryuseEffect 후크에 대한 종속성으로 전달했습니다. 그렇게 함으로써 쿼리 변경 사항을 추적하도록 useEffect 에 지시합니다. 이전 query 값이 현재 값과 useEffect 가 다시 호출됩니다.

즉, 필요에 따라 구성 요소에 여러 status 를 설정하고 있습니다. 이렇게 하면 일부 유한 상태 status 를 기반으로 화면에 일부 메시지를 더 잘 전달할 수 있기 때문입니다. 유휴 상태에서 사용자에게 검색 상자를 사용하여 시작할 수 있음을 알릴 수 있습니다. 가져오는 상태에서 스피너 를 표시할 수 있습니다. 그리고 가져온 상태에서 데이터를 렌더링합니다.

fetched 상태를 설정하는 동안 데이터가 비어 있는 결과로 발생하는 깜박임을 방지할 수 있도록 fetched 상태를 설정하기 전에 데이터를 설정하는 것이 중요합니다.

사용자 정의 후크 생성

"사용자 정의 후크는 이름이 'use'로 시작하고 다른 후크를 호출할 수 있는 JavaScript 함수입니다."

— 리액트 문서

이것이 바로 진정한 의미이며 JavaScript 기능과 함께 앱의 여러 부분에서 일부 코드를 재사용할 수 있습니다.

React Docs의 정의가 이를 제공했지만 실제로 카운터 사용자 지정 후크와 함께 작동하는 방법을 살펴보겠습니다.

 const useCounter = (initialState = 0) => { const [count, setCount] = useState(initialState); const add = () => setCount(count + 1); const subtract = () => setCount(count - 1); return { count, add, subtract }; };

여기에는 선택적 인수를 받아 값을 상태로 설정하고 업데이트하는 데 사용할 수 있는 addsubtract 메서드를 추가하는 일반 함수가 있습니다.

카운터가 필요한 앱의 모든 곳에서 일반 함수처럼 useCounter 를 호출하고 initialState 를 전달하여 어디에서 계산을 시작해야 하는지 알 수 있습니다. 초기 상태가 없으면 기본값은 0입니다.

실제로 작동하는 방법은 다음과 같습니다.

 import { useCounter } from './customHookPath'; const { count, add, subtract } = useCounter(100); eventHandler(() => { add(); // or subtract(); });

여기서 우리가 한 것은 우리가 선언한 파일에서 사용자 정의 후크를 가져와 앱에서 사용할 수 있도록 하는 것입니다. 초기 상태를 100으로 설정하여 add() 를 호출할 때마다 count 를 1씩 증가시키고, subtract() 를 호출할 때마다 count 를 1로 감소시킵니다.

useFetch 후크 만들기

이제 간단한 사용자 지정 후크를 만드는 방법을 배웠으므로 데이터를 사용자 지정 후크로 가져오는 논리를 추출해 보겠습니다.

 const useFetch = (query) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]); return { status, data }; };

query 를 받아 statusdata 를 반환하는 함수라는 점을 제외하면 위에서 했던 것과 거의 동일합니다. 그리고 이것은 React 애플리케이션의 여러 구성 요소에서 사용할 수 있는 useFetch 후크입니다.

이것은 작동하지만 현재 이 구현의 문제는 Hacker News에만 해당되므로 useHackerNews 라고 부를 수 있다는 것입니다. 우리가 하려는 것은 모든 URL을 호출하는 데 사용할 수 있는 useFetch 후크를 만드는 것입니다. 대신 URL을 사용하도록 수정해 보겠습니다.

 const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch(url); const data = await response.json(); setData(data); setStatus('fetched'); }; fetchData(); }, [url]); return { status, data }; };

이제 useFetch 후크는 일반적이며 다양한 구성 요소에서 원하는 대로 사용할 수 있습니다.

소비하는 한 가지 방법은 다음과 같습니다.

 const [query, setQuery] = useState(''); const url = query && `https://hn.algolia.com/api/v1/search?query=${query}`; const { status, data } = useFetch(url);

이 경우 query 값이 truthy 이면 URL을 설정하고 그렇지 않은 경우 후크에서 처리되므로 undefined를 전달하는 것이 좋습니다. 효과는 상관없이 한 번만 실행됩니다.

가져온 데이터를 메모화하기

메모이제이션은 우리가 어떤 초기 단계에서 그것을 가져오기 위한 일종의 요청을 한 경우 우리가 해커뉴스 엔드포인트에 hackernews 않도록 하기 위해 사용하는 기술입니다. 값비싼 가져오기 호출의 결과를 저장하면 사용자가 로드 시간을 약간 절약할 수 있으므로 전체 성능이 향상됩니다.

참고 : 자세한 내용은 메모이제이션에 대한 Wikipedia의 설명을 참조하세요.

어떻게 할 수 있는지 알아봅시다!

 const cache = {}; const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache[url]) { const data = cache[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

여기에서 URL을 데이터에 매핑합니다. 따라서 기존 데이터를 가져오도록 요청하면 로컬 캐시에서 데이터를 설정하고, 그렇지 않으면 요청을 진행하고 캐시에 결과를 설정합니다. 이렇게 하면 로컬에서 데이터를 사용할 수 있을 때 API 호출을 하지 않습니다. 또한 URL이 falsy 인 경우 효과를 없애고 있음을 알 수 있으므로 존재하지 않는 데이터 가져오기를 진행하지 않도록 합니다. useEffect 후크 전에는 할 수 없습니다. 이는 후크 규칙 중 하나인 항상 최상위 수준에서 후크를 호출하는 것입니다.

다른 범위에서 cache 를 선언하는 것은 작동하지만 우리의 후크가 순수 함수의 원칙에 어긋나게 만듭니다. 게다가, 우리는 더 이상 구성 요소를 사용하고 싶지 않을 때 React가 혼란을 정리하는 데 도움이 되도록 하고 싶습니다. 이를 달성하는 데 도움이 되도록 useRef 를 살펴보겠습니다.

useRef 로 데이터 메모하기

" useRef.current property 에 변경 가능한 값을 담을 수 있는 상자와 같습니다."

— 리액트 문서

useRef 를 사용하면 변경 가능한 값을 쉽게 설정하고 검색할 수 있으며 그 값은 구성 요소의 수명 주기 동안 지속됩니다.

캐시 구현을 useRef 마법으로 교체합시다!

 const useFetch = (url) => { const cache = useRef({}); const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache.current[url]) { const data = cache.current[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

여기에서 캐시는 이제 초기 값으로 빈 객체가 있는 useFetch 후크에 있습니다.

마무리

글쎄, 나는 가져온 상태를 설정하기 전에 데이터를 설정하는 것이 좋은 생각이라고 말했지만 우리가 가질 수 있는 두 가지 잠재적인 문제도 있습니다.

  1. 가져오기 상태에 있는 동안 데이터 배열이 비어 있지 않기 때문에 단위 테스트가 실패할 수 있습니다. React는 실제로 상태 변경을 일괄 처리할 수 있지만 비동기식으로 트리거되면 그렇게 할 수 없습니다.
  2. 우리 앱은 예상보다 더 많이 다시 렌더링됩니다.

useFetch 후크를 마지막으로 정리하겠습니다. useStateuseReducer 로 전환하여 시작하겠습니다. 어떻게 작동하는지 봅시다!

 const initialState = { status: 'idle', error: null, data: [], }; const [state, dispatch] = useReducer((state, action) => { switch (action.type) { case 'FETCHING': return { ...initialState, status: 'fetching' }; case 'FETCHED': return { ...initialState, status: 'fetched', data: action.payload }; case 'FETCH_ERROR': return { ...initialState, status: 'error', error: action.payload }; default: return state; } }, initialState);

여기서 우리는 각각의 개별 useState 에 전달한 초기 값인 초기 상태를 추가했습니다. useReducer 에서 수행하려는 작업 유형을 확인하고 이에 따라 적절한 값을 설정합니다.

이것은 불가능 상태와 불필요한 재렌더링을 방지하기 위해 상태와 데이터를 동시에 설정하기 때문에 앞서 논의한 두 가지 문제를 해결합니다.

한 가지만 더 남았습니다. 부작용을 정리하는 것입니다. Fetch.AI는 해결되거나 거부될 수 있다는 점에서 Promise API를 구현합니다. 일부 Promise 가 방금 해결되었기 때문에 구성 요소가 마운트 해제된 동안 후크가 업데이트를 시도하면 React Can't perform a React state update on an unmounted component.

useEffect 클린업으로 어떻게 고칠 수 있는지 봅시다!

 useEffect(() => { let cancelRequest = false; if (!url) return; const fetchData = async () => { dispatch({ type: 'FETCHING' }); if (cache.current[url]) { const data = cache.current[url]; dispatch({ type: 'FETCHED', payload: data }); } else { try { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; if (cancelRequest) return; dispatch({ type: 'FETCHED', payload: data }); } catch (error) { if (cancelRequest) return; dispatch({ type: 'FETCH_ERROR', payload: error.message }); } } }; fetchData(); return function cleanup() { cancelRequest = true; }; }, [url]);

여기에서는 효과 내부에 정의한 후 cancelRequesttrue 로 설정합니다. 따라서 상태 변경을 시도하기 전에 먼저 구성 요소가 마운트 해제되었는지 확인합니다. 마운트 해제된 경우 상태 업데이트를 건너뛰고 마운트 해제되지 않은 경우 상태를 업데이트합니다. 이렇게 하면 React 상태 업데이트 오류가 해결되고 구성 요소의 경쟁 조건도 방지됩니다.

결론

구성 요소에서 데이터를 가져오고 캐시하는 데 도움이 되는 몇 가지 후크 개념을 살펴보았습니다. 또한 앱에서 많은 문제를 방지하는 데 도움이 되는 useEffect 후크를 정리했습니다.

질문이 있는 경우 아래의 댓글 섹션에 언제든지 질문을 남겨주세요!

  • 이 기사에 대한 리포지토리 참조 →

참고문헌

  • "Hook 소개", React Docs
  • "React Hooks API 시작하기", Shedrack Akintayo
  • "React Hooks의 모범 사례", Adeneye David Abiodun
  • "함수형 프로그래밍: 순수 함수", Arne Brasseur