리액트 훅 소개

맛없는콩두유·2022년 10월 2일
0
post-thumbnail

리액트 훅에 대해서 복습해보겠습니다!

리액트 훅이란 무엇인가

함수형 컨포넌트와 클래스형 컴포넌트로 만들 수 있습니다.

함수형 컴포넌트는 프로퍼티를 받고 JSX를 반환했었습니다.

리액트 훅을 사용할 걸면 클래스형 컴포넌트는 필요 없습니다.
또 작업량이 많다는 것이 클래스형 컴포넌트의 단점입니다.

useState() 시작하기

현재 두 개의 입력창이 있습니다. 우리는 이 입력한 것을 useState 훅을 이용해 업데이트 해보도록 하겠습니다!

  • ingredientFrom.js
import React, { useState } from "react";

useState를 import 해주고,

const [inputState, setInputState] = useState({ title: "", amount: "" });

inputState는 현재 입력 값이 되고, setInputState는 업데이트할 입력 값이 되는 것입니다. 그리고 useState()에는 업데이트할 input 값이 두개 이기 때문에 객체로 title과 amount를 빈 문자열로 초기화 해줍니다!

<input
              type="text"
              id="title"
              value={inputState.title}
              onChange={(event) => {
                const newTitle = event.target.value;
                setInputState((prevInputState) => ({
                  title: newTitle,
                  amount: prevInputState.amount,
                }));
              }}
            />

value로 현재 title 값을 넣어주고
onChange 값이 바뀔 떄 마다 현재 값을 newTitle에 넣어주고 업데이트할 값들을 setInputState를 통해 매개변수로 preInputState인 이전 값을 그대로 가져와서 그 title에는 새 값을 amount에는 기존 값을 넣어주면 됩니다!

<input
              type="number"
              id="amount"
              value={inputState.amount}
              onChange={(event) => {
                const newAmount = event.target.value;
                setInputState((prevInputState) => ({
                  amount: newAmount,
                  title: prevInputState.title,
                }));
              }}
            />

amount에도 마찬가지로 넣어줍니다!

이제 useState에 대해 전반적으로 알게되었습니다. 하지만 거슬리는 부분이 한 가지 있습니다!
바로 useState에 기본값을 객체로 뒀다는 것입니다. 현재 form의 input 값들이 적어서 괜찮지만 input 값들이 많으면 prevInputState를 여러 개 적어야 하기 떄문에 복잡해집니다!

대신 useState 훅을 여러 개 사용하면 됩니다!

  • ingredientForm.js
  const [enteredTitle, setEnteredTitle] = useState("");
  const [enteredAmount, setEnteredAmount] = useState("");

Title과 Amount에 각각 useState hook을 사용하여 따로 관리합니다!

value={enteredTitle}
 onChange={(event) => {
          setEnteredTitle(event.target.value);
  }}
  
  
value={enteredAmount}
  onChange={(event) => {
          setEnteredAmount(event.target.value);
 }}

이렇게 하면 prevInputState를 쓰지 않고 쉽게 관리할 수 있게 됩니다!

훅 규칙

useState()는 첫 번째 state의 포인터고 두 번쨰 요소는 상태 업데이트 함수입니다!

모든 훅에 적용되는 규칙이 있습니다
반드시 컴포넌트 안에서 사용이 가능하다는 것입니다! 조건문이나 다른 함수에서 사용이 불가능합니다!

컴포넌트 간에 State 데이터 전달하기

이제 다음으로 넘어가서 Add Ingredient 버튼이 동작하고 ingredient가 추가되도록 만들어보겠습니다!

ingredient는 ingredients.js에서 관리합니다!

현재 List들은 ingredientList.js에서 map으로 관리합니다.

{props.ingredients.map(ig => (
          <li key={ig.id} onClick={props.onRemoveItem.bind(this, ig.id)}>
            <span>{ig.title}</span>
            <span>{ig.amount}x</span>
          </li>
        ))}

id와 title, amount가 피룡한 것을 볼 수 있습니다.

  • ingredients.js
import React, { useState } from "react";

const [userIngredients, setUserIngredients] = useState([]);

관리할 state를 초기화하고

const addIngredientHandler = (ingredient) => {
    setUserIngredients((prevIngredients) => [
      ...prevIngredients,
      { id: Math.random().toString(), ...ingredient },
    ]);
  };

eventhandler를 설정하여 매개변수로 ingredient가 들어오면 setUserIngredients로 업데이트하여 prevIngredients인 현재 값을 매개변수로 설정하고 배열로 ...prevIngredients로 첫번째 이전 속성들을 다 불러오고 객체로 id값과 title amount가 필요하기 때문에 id는 랜덤 값 ...ingredient로 나머지 속성 값인 title과 amount를 불러옵니다!

<IngredientForm onAddIngredient={addIngredientHandler} />

<IngredientList ingredients={userIngredients} onRemoveItem={() => {}} />

그리고 이것을 IngredientFrom과 ingredientList에 전달하여

  • ingredientForm.js
  const submitHandler = (event) => {
    event.preventDefault();
    props.onAddIngredient({ title: enteredTitle, amount: enteredAmount });
  };

입력한 title과 amount를 제출할 때 title과 amouint를 onAddingredient로 보내주면 ingredients에 addIngredientHandler의 매개변수인 ingredient로 들어오게 됩니다!

짜파게티 5개가 추가된 것을 볼 수 있습니다!

Http 요청 보내기

이를 위해 테스트용 백엔드 Firebase를 이용하겠습니다!
Realtime Database를 이용해 요청을 보내면 됩니다.

두 가지 방법이 있지만 fetch를 이용해보겠습니다!

  • ingredients.js
  const addIngredientHandler = ingredient => {
    fetch('https://react-hooks-update.firebaseio.com/ingredients.json', {
      method: 'POST',
      body: JSON.stringify(ingredient),
      headers: { 'Content-Type': 'application/json' }
    })
      .then(response => {
        return response.json();
      })
      .then(responseData => {
        setUserIngredients(prevIngredients => [
          ...prevIngredients,
          { id: responseData.name, ...ingredient }
        ]);
      });
  };

데이터를 저장하기 위해 POST 방식을 이용해야 합니다!

그리고 then에 reponse 요청이 오면 요청이 완료되면 then 안에 욫엉을 json형태로 바꿔준 후
또 다른 then으로 Firebase에 있는 자체적인 데잉터인 responseData 객체인데 객체안에는 name이 들어있어서 id를 reponseData.name으로 바꿔주면 됩니다.

두 개가 추가되었는데,


Firebase에도 데이터가 들어온 것이 확인이 됩니다!

이제 새로고침해도 데이터가 남아있는 상태를 유지할 수 있게 해보겠습니다!

useEffect() 및 데이터 로딩

ingredients가 렌더링 될 떄 마다 모든 재료 목록을 가져와야 됩니다. 바로 useEffect 훅을 사용하면 됩니다! useEffect는 부수효과를 관리하기 떄문에 붙여졌습니다. 대표적인 것으로 HTTP 요청이 부수 효과죠!

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

useEffect를 import 해주고

 useEffect(() => {
    fetch('https://react-hooks-update.firebaseio.com/ingredients.json')
      .then(response => response.json())
      .then(responseData => {
        const loadedIngredients = [];
        for (const key in responseData) {
          loadedIngredients.push({
            id: key,
            title: responseData[key].title,
            amount: responseData[key].amount
          });
        }
        setUserIngredients(loadedIngredients);
      });
  }, []);

useEffect안에 fetch하는 로직을 넣어줍니다! 그리고 then에 Firebase의 responseData 객체를 불러와서

빈 배열을 만들고 responseData안에 있는 key를 전부 반복시킨 후 새 배열에 push를 할 것이고 id와 title amount를 push하고 setUseIngredients로 업데이트하면 됩니다!

그리고 useEffect()는 두 번째 인자로 의존성 배열을 가지는데 이 것으로 렌더링을 몇번할 지 조절할 수 있습니다. 현재 빈 배열 []이기 떄문에 최초로 렌더링된 시점에 한 번 실행된다는 의미입니다! 이 것을 안붙이면 무한 루프에 빠지게 됩니다!

useEffect() 종속성 이해하기

하나의 동일한 컴포너늩 안에서 useEffect()를 여러 번 호출할 수 있습니다.

  • ingredients.js
useEffect(() => {
    console.log('RENDERING INGREDIENTS', userIngredients);
  }, [userIngredients]);

이 함수는 이제 userIngredients가 변경될 때만 실행될 겁니다.

useEffect() 더 알아보기

Serach 컴폰너트로 가보겠습니다 Search 컴폰넌트는 필터 입력창을 담당합니다.

먼저 사용자가 입력한 값을 관리하기 위해 useState를 가져와야합니다.

  • Search.js
import React, { useState } from 'react';

...

 const [enteredFilter, setEnteredFilter] = useState('');

...
 
 <input
            type="text"
            value={enteredFilter}
            onChange={event => setEnteredFilter(event.target.value)}
          />

이번에는 사용자가 뭔가 입력할 때 필터링한 데이터를 Firebase에서 가져오려고 합니다.

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

그러기 위해서는 onChange에 등록한 함수에서 키 입력이 들어올 떄마다 HTTP 요청을 보내면 되겠습니다. 그러기 위해선 useEffect Hook이 필요합니다.


  const { onLoadIngredients } = props;

...

  useEffect(() => {
    const query =
      enteredFilter.length === 0
        ? ''
        : `?orderBy="title"&equalTo="${enteredFilter}"`;
    fetch('https://react-hooks-update.firebaseio.com/ingredients.json' + query)
      .then(response => response.json())
      .then(responseData => {
        const loadedIngredients = [];
        for (const key in responseData) {
          loadedIngredients.push({
            id: key,
            title: responseData[key].title,
            amount: responseData[key].amount
          });
        }
        onLoadIngredients(loadedIngredients);
      });
  }, [enteredFilter, onLoadIngredients]);

여기서 포인트는 query입니다. Firebase에서 제공하는 query문을 이용하면 되고 fetch에 + query를 해주면 됩니다. 그리고 onLoadIngredients는 Ingredients에서 관리하는 것이기 때문에 Serach컴폰너트로 보내주면 됩니다!

그리고 보내준 것을 의존성 배열로 추가시켜줍니다.

  • Ingredients.js
const filteredIngredientsHandler = filteredIngredients => {
    setUserIngredients(filteredIngredients);
  }
  
  ...
  
  <Search onLoadIngredients={filteredIngredientsHandler} />

Serach에서 입력한 값을 filteredIngredients라는 매개변수로 받아 업데이트 해주면 됩니다!

하지만 여기서 문제가 발생합니다! 무한루프에 다시 빠지게 되었습니다.

useCallback()이란 무엇인가

  • Search.js
useEffect(() => {

...

,[enteredFilter, onLoadIngredients]);

무한 루프는 onLoadIngredients 의존성 값떄문에 발생했습니다.
이 Effect는 enteredFilter나 onLoadingredients가 변경될 떄마다 실행됩니다. Search가 처음 렌더링될 떄 데이터를 가져올 것입니다. 그 다음 enteredFilter가 변경될 때마다 실행될 겁니다. 또는 onLoadingredinets가 바뀌어도 실행되죠.

  • Ingredients.js

  const filteredIngredientsHandler = useCallback(filteredIngredients => {
    setUserIngredients(filteredIngredients);
  }, []);
  
 <Search onLoadIngredients={filteredIngredientsHandler} />

결국엔 Ingredients에서 Serach 컴포넌트에 onLoadIngredinets= {filteredIngredinetsHadler}
를 봅내는데 이 떄 filteredIngredinetsHandler 함수도 다시 렌더링 돼서 새로운 값을 Serach에 넘겨줍니다.

그러면 Serach.js의 Effect에서 의존된 onLoadIngredients는 이전에 Effect를 실행했을 떄와 값이 달라졌기 떄문에 Effect가 다시 실행됩니다. 부모 컴포넌트인 Ingredients.js가 리렌더링 되면서 모든 함수들이 새로 생성되고 값이 달라졌으니까 Effect와 Serach 컴포넌트를 다시 실행하는 겁니다. 이를 막기 위해 리액트가 제공하는 것은 useCallback 훅입니다. 함수 하나를 품을 수 있습니다.

  const filteredIngredientsHandler = useCallback(filteredIngredients => {
    setUserIngredients(filteredIngredients);
  }, []);

useCallback도 useEffect처럼 두 개의 인자를 받을 수 있는데 첫 번쨰 인자는 함수였고, 두 번쨰 인자는 의존성 ㄱ밧을 담은 배열입니다. setUserIngredients는 state가 관리하기 떄문에 의존할 것이 없어서 빈 배열 []로 냅두면 됩니다!

이렇게 되면 리랜더링될 떄 filtered함수가 재실행되지 않고 Serach로 넘겨줄 떄 이전에 렌더링할 떄 사용헀던 함수와 같은 함수가 됩니다!

현재 두 번의 렌더링이 발생하는데 이 것을 고치기 위해서 Ingredients.js에 useEffect의 fetch 구문을 삭제하면 됩니다! 이미 Serach에서 fetch한 것들을 가져왔기 떄문에 필요가 없습니다!

Refs 및 useRef() 작업하기

현재 키가 입력 될 때 마다 HTTP 요청하는 문제가 발생합니다.

입력창의 내용을 확인해 내용이 타이머가 시작된 시점에 입력된 내용과 같으면 사용자가 멈췄다는 뜻이니까 그 때만 요청을 보내는 것입니다!

useEffect() 안에 setTimeout()을 이용하면 되겠습니다!

Search.js

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

  const inputRef = useRef();

  useEffect(() => {
    const timer = setTimeout(() => {
      if (enteredFilter === inputRef.current.value) {
        const query =
          enteredFilter.length === 0
            ? ''
            : `?orderBy="title"&equalTo="${enteredFilter}"`;
        fetch(
          'https://react-hooks-update.firebaseio.com/ingredients.json' + query
        )
          .then(response => response.json())
          .then(responseData => {
            const loadedIngredients = [];
            for (const key in responseData) {
              loadedIngredients.push({
                id: key,
                title: responseData[key].title,
                amount: responseData[key].amount
              });
            }
            onLoadIngredients(loadedIngredients);
          });
      }
    }, 500);
    return () => {
      clearTimeout(timer);
    };
  }, [enteredFilter, onLoadIngredients, inputRef]);

enteredFilter는 500ms 전에 입력됐던 값입니다. 그 값을 현재 입력된 값이랑 비교를 하면 됩니다! 현재 입력된 값을 가져오려면 useRef hook이 필요합니다!

enteredFilter인 타이머를 설정한 시점의 값과 입력된 값이 같다면 그 사이에 값이 변하지 않았다는 뜻입니다.

그리고 inputRef도 useRffect안에서 의존성 배열에 들어가야합니다!

이렇게하면 useEffect 실행과 동시에 적었던 입력과 500ms 지나서 입력한 값이 같을 때만 요청을 보내는 겁니다!

또, const timer로 setTimeout을 변수로 넣어서 useEffect는 인수로 함수를 받고, 그 함수는 무언가를 반환할 수 있습니다. 그 return에는 반드시 함수여야하고, Effect가 다시 실행되기 전에 실행됩니다. 이 것이 클린업 함수인데 이렇게 하면 한 번에 하나의 타이머만 사용해서 불필요한 타이머를 메모리에 사용하지 않게 합니다!!

Ingredients 삭제

현재 삭제를 하려면 ingredients.js에
removeIngredientHandler의 함수에 로직을 보겠습니다! 현재는 백엔드에 삭제가 되지 않고 로컬에서만 삭제가 됩니다.

const removeIngredientHandler = ingredientId => {
 fetch(
      `https://react-hooks-update.firebaseio.com/ingredients/${ingredientId}.jon`,
      {
        method: 'DELETE'
      }
    ).then(response => {
      setUserIngredients(prevIngredients =>
        prevIngredients.filter(ingredient => ingredient.id !== ingredientId)
      );
    })
    };

삭제할 것이기 때문에 method만 DELETE로 보내고 header와 bodt는 상관 없고 무엇을 삭제할 것인지 fetch에 ``(백틱)과 함께 표시해주면 됩니다!

그리고 then으로 요청을 받아 삭제한 것을 업데이트하는 logic을 추가해주면 됩니다.

클릭하면 삭제가되고, firebase에도 없어지는 것이 확인이 됩니다!

다음으로 데이터가 전송될 때나 그러지 못했을 때 로딩 화면과 에러메시지를 modal 창으로 표시해보겠습니다!

로드 오류 및 state 일괄 처리 하기

loading 화면을 보여주기 위해서는 ingredients.js에 또 다른 state가 필요합니다.

  • Ingredienst.js
const [isLoading, setIsLoading] = useState(false);

...

 const addIngredientHandler = ingredient => {
    setIsLoading(true);
    fetch('https://react-hooks-update.firebaseio.com/ingredients.json', {
      method: 'POST',
      body: JSON.stringify(ingredient),
      headers: { 'Content-Type': 'application/json' }
    })
      .then(response => {
        setIsLoading(false);
        return response.json();
      })

재료 추가할 떄 isLoading을 true로하고 응답을 받으면 다시 false로 변경합니다. 추가가 완료됐으니까요!

IngredientForm에 로딩표시인 loadingInticator을 표시 해보겠습니다 loading prop로 {isloading}을 보내겠습니다

  return (
    <div className="App">
      {error && <ErrorModal onClose={clearError}>{error}</ErrorModal>}

	...
    }

그리고 IngredientForm.js에서 loadingIndicator을 import 해주고 button 아래 쪽에
props.loading이 true이면 LoadingIndicator가 보이게 설정하겠습니다.

import LoadingIndicator from '../UI/LoadingIndicator';

...


<button type="submit">Add Ingredient</button>
            {props.loading && <LoadingIndicator />}

이제 Ingredients.js에서 재료가 삭제될 때도 로딩을 설정해줍시다.

  • Ingredients.js
  const removeIngredientHandler = ingredientId => {
    setIsLoading(true);
    fetch(
      `https://react-hooks-update.firebaseio.com/ingredients/${ingredientId}.jon`,
      {
        method: 'DELETE'
      }
    ).then(response => {
      setIsLoading(false);
      setUserIngredients(prevIngredients =>
        prevIngredients.filter(ingredient => ingredient.id !== ingredientId)
      );
    })
  };

이제 에러처리를 해보겠습니다. 재료를 삭제하다 오류가 발생했다고 가정해봅시다. Firebase는 굉장히 안정적이라 거기서 어류가 없을 건데 fetch하는 과정에서 URL을 다르게 적어 오류를 발생시켜 보겠습니다.

  import ErrorModal from '../UI/ErrorModal';
  
  ...
   const [error, setError] = useState();
   
   ...

  const removeIngredientHandler = ingredientId => {
    setIsLoading(true);
    fetch(
      `https://react-hooks-update.firebaseio.com/ingredients/${ingredientId}.jon`,
      {
        method: 'DELETE'
      }
    ).then(response => {
      setIsLoading(false);
      setUserIngredients(prevIngredients =>
        prevIngredients.filter(ingredient => ingredient.id !== ingredientId)
      );
    }).catch(error => {
      setError('Something went wrong!');
      setIsLoading(false);
    });
  };
  
  ...
  
  
   {error && <ErrorModal onClose={clearError}>{error}</ErrorModal>}

오류 처리는 catch 문에서 잡고 ErrorModal을 import 하여 그리고 error를 다룰 State도 만들어 줍니다. 그리고 catch문에 setError로 미리 작성한 에러 문구를 보여주겠습니다.

error가 설정되면 ErrorModal을 띄울 수 있는데 error가 참이면 ErrorModal이 보여지게 되고 여기에 메시지가 저장된 error를 넣겠습니다. 배경이나 OK버튼이 클릭됐을 떄 onClose()를 호출합니다. onCLose 프로퍼티에 clearError함수를 포인터하겠습니다. setError를 Null로 하면 error가 true가 아니니깐 모달창이 닫히는 효과가 나타나겠습니다. 그리고 catch 문에 error를 설정하는 곳에 setIsLoading을 false로 설정하면 에러 창이 뜨자마자 멈추는 것을 볼 수 있습니다!

useReducer() 이해하기

현재 Ingredients.js에서 state들은 서로 연관되어있고, 이렇게 예전 state를 사용하거나, 다른 state의 새로운 상태를 기반으로 새롭게 state를 업데이트해야할 경우 useState()를 사용하는 것보다 useReducer를 대신 사용하는 겁니다!!

import React, { useReducer, useState, useEffect, useCallback } from "react";

리듀서는 여러 개의 입력을 받아 하나의 결과를 반환하는 함수입니다. 상태를 업데이트할 때 어떤 식으로 상태를 변경할 건지 정의할 수 이쎅 해줍니다.

리듀서부터 만들겠습니다.

const ingredientReducer = (currentIngredients, action) => {
  switch (action.type) {
    case "SET":
      return action.ingredients;
    case "ADD":
      return [...currentIngredients, action.ingredient];
    case "DELETE":
      return currentIngredients.filter((ing) => ing.id !== action.id);
    default:
      throw new Error("Should not get there!");
  }
};

이 함수는 리액트로부터 자동으로 2개의 인자를 받게 됩니다.
먼저 state를 받죠, 그리고 action 입니다 action은 state를 업데이트하는데 중요합니다.

그리고 Switch문을 사용해 액션 탕비에 따라 서로 다른 코드를 수행하도록 합니다!

그리고 useReducer를 불러옵니다.

useReducer는 인수로 함수를 받아올 수 있는데 위에 설정한 함수를 첫 번쨰 인수로 불러올 수 있고, 두 번쨰 인수는 선택적인데 빈 배열로 냅두겠습니다. 그러면 currentIngredients에 현재 재료 목록이 들어올 빈 배열을 만들겟습니다!

const Ingredients = () => {
  const [userIngredients, dispatch] = useReducer(ingredientReducer, []);
  // const [userIngredients, setUserIngredients] = useState([]);

그리고 state를 가질 수 있는데 여기서 두 번쨰 인자로 dispatch로 action을 관리할 수 있습니다!

const filteredIngredientsHandler = useCallback((filteredIngredients) => {
    // setUserIngredients(filteredIngredients);
    dispatch({ type: "SET", ingredients: filteredIngredients });
  }, []);




const addIngredientHandler = (ingredient) => {

...


.then((responseData) => {
	dispatch({
          type: "ADD",
          ingredient: { id: responseData.name, ...ingredient },
        });




const removeIngredientHandler = (ingredientId) => {

...

.then((response) => {
        setIsLoading(false);
     
        dispatch({ type: "DELETE", id: ingredientId });
      })

이렇게 각각에 맞는 함수에 set대신 dispatch를 활용하여 type과 property를 맞게 적어줍니다!

Http State에 대해 useReducer() 사용하기

loading과 error도 http state와 관련이 있습니다. 현재로도 좋지만 리듀서를 사용하면 코드를 해석하시각 쉬워질 겁니다.

리듀서부터 만들어 보겠습니다.
그리고 type은 총 3개의 케이스가 필요할 것입니다.

항상 신호를 보낼 fetch 부분과 response 부분과 오류를 잡을 catch부분이 필요합니다.

  • Ingredients.js
const httpReducer = (curHttpState, action) => {
  switch (action.type) {
    case 'SEND':
      return { loading: true, error: null };
    case 'RESPONSE':
      return { ...curHttpState, loading: false };
    case 'ERROR':
      return { loading: false, error: action.errorMessage };
    case 'CLEAR':
      return { ...curHttpState, error: null };
    default:
      throw new Error('Should not be reached!');
  }
};

이제 useReducer()로 상태를 초기화 해볼게요.

const Ingredients = () => {
...
  const [httpState, dispatchHttp] = useReducer(httpReducer, {
    loading: false,
    error: null
  });

addIngredientHandler에 전송하는 부분에 dispatcher와 응답받은 부분에 dispatch를 각각 set대신 붙입니다.

 const addIngredientHandler = ingredient => {
    dispatchHttp({ type: 'SEND' });
    fetch('https://react-hooks-update.firebaseio.com/ingredients.json', {
      method: 'POST',
      body: JSON.stringify(ingredient),
      headers: { 'Content-Type': 'application/json' }
    })
      .then(response => {
        dispatchHttp({ type: 'RESPONSE' });
        return response.json();
      })

remove에서도 마찬가지로 SEND와 REPONSE Type에 맞게 작성하고 catch 부분에 ERROR type을 적어줍니다.

const removeIngredientHandler = ingredientId => {
    dispatchHttp({ type: 'SEND' });
    fetch(
      `https://react-hooks-update.firebaseio.com/ingredients/${ingredientId}.json`,
      {
        method: 'DELETE'
      }
    )
      .then(response => {
        dispatchHttp({ type: 'RESPONSE' });
        // setUserIngredients(prevIngredients =>
        //   prevIngredients.filter(ingredient => ingredient.id !== ingredientId)
        // );
        dispatch({ type: 'DELETE', id: ingredientId });
      })
      .catch(error => {
        dispatchHttp({ type: 'ERROR', errorMessage: 'Something went wrong!' });
      });
  };

Error를 clear하는 함수에서도 맞는 타입을 적어주고

 const clearError = () => {
    dispatchHttp({ type: 'CLEAR' });
  };

const [httpState, dispatchHttp] = useReducer(httpReducer, {

...
으로 선언한 Reducer의 초기 httpState의 에러 상태에 따라 다르게 보여지도록 선언합니다!

return (
    <div className="App">
      {httpState.error && (
        <ErrorModal onClose={clearError}>{httpState.error}</ErrorModal>
      )}

이렇게 Reducer로 관리를 하면 코드를 보기 쉽게 관리할 수 있습니다!


remove했을 때 요청 주소를 다르게 적어 에러메시지도 잘 뜨는 것이 확인이 됩니다!

useContext() 작업하기

이제 ContextAPI를 살펴보겠습니다.

Auth.js 파일을 볼게요!

로그인 화면이 있는데, 아직 기능은 없습니다 이 화면을 보여주겠습니다! 로그인한 경우에 재료 목록을 보이도록요!

이를 위해 App.js 파일을 활용할 겁니다.

  • App.js

여러 컴포넌트로 구성되어있고 props로 연결시켜주기가 복잡할 떄 Context를 사용하면 유용합니다!

context 폴더를 만들고 auth-context.js를 만들겠습니다!

  • auth-context.js
import React, { useState } from 'react';

export const AuthContext = React.createContext({
  isAuth: false,
  login: () => {}
});

Context를 불러오기 위해 React에서 불러옵니다.

먼저 로그인 상태를 false로 설정하고
login 함수를 설정하면 자동완성 기능을 쓸 수 있습니다!
나중에 실제 함수를 구현해 덮어쓸겁니다.

다음으로 같은 파일에 다른 컴포넌트인 AuthContextProvider로 만들겠습니다.

const AuthContextProvider = props => {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const loginHandler = () => {
    setIsAuthenticated(true);
  };

  return (
    <AuthContext.Provider
      value={{ login: loginHandler, isAuth: isAuthenticated }}
    >
      {props.children}
    </AuthContext.Provider>
  );
};

export default AuthContextProvider;

여기서 Provider를 이용해 props.children을 이용해 어느 컴포넌트에서도 사용할 수 있게 만듭니다.

로그인 상태를 관리하기 위해 useStste를 쓸 것
초기 값을 false로 설정합니다.

긜고 value로 login과 isAuth를 넘겨주면 필요로하는 모든 컴포넌트가 업데이트된 값을 받을 겁니다.

이제 이 Provider를 우리 앱 루트에 있는 index.js 파일에서 사용할 수 있습니다.

  • index.js
import AuthContextProvider from "./context/auth-context";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <AuthContextProvider>
    <App />
  </AuthContextProvider>
);

이렇게하면 앱 어디서든 이 컨텍스트를 사용할 수 있습니다!

App.js에서 useContext hook을 이용해 AuthContext를 사용하겠습니다!

useContext와 AuthContext를 import 해줍니다.

  • App.js
import React, { useContext } from "react";

import Ingredients from "./components/Ingredients/Ingredients";
import Auth from "./components/Auth";
import { AuthContext } from "./context/auth-context";

const App = (props) => {
  const authContext = useContext(AuthContext);

  let content = <Auth />;
  if (authContext.isAuth) {
    content = <Ingredients />;
  }

  return content;
};

export default App;

그리고 isAuth에 따라 Auth가 보여질 지 Ingredients가 보여질 지를 정해줍니다

그리고 로그인 버튼을 클릭했릉 떄 동작하게 만드려면

auth-context.jd에 있는 loginHandler함수에 연결해야 합니다.

작업을 위해 Auth.js로 가서 AuthContext와 useContext를 import 해줍니다.

  • Auth.js
 const authContext = useContext(AuthContext);

  const loginHandler = () => {
    authContext.login();
    
    ...
    
   <button onClick={loginHandler}>Log In</button>
  };

useContext로 인해 AUthContext를 연결해주고
loginHandler에 AuthContext에서 value로 보내준 login property를 연결시켜주면 끝!!!

첫 화면 렌더링이 로그인 버튼이 생기고 로그인 버튼을 누르면
재료 목록들이 보이는 컴포넌트로 렌더링되는 것을 볼 수 있다!

useMemo()를 사용한 성능 최적화

IngredientForm.js 에서
불필요한 렌더링을 없애려면

addHandler 함수 전체를 useCallback으로 붂어줍니다.

  • Ingredients.js
const addIngredientHandler = useCallback((ingredient) => {

...


},{});

마찬가지로 IngredientList.js에 넘겨주는 불필요한 렌더링을 없애려면

removeHandler 전체를 useCallback으로 묶어줍니다.

const removeIngredientHandler = useCallback((ingredient) => {

...


},{});

그리고 IngredientList.js로 가서 React.memo를 사용해서 하면 해결이 됩니다.
하지만 저희는 useMoemo 훅을 사용하겠습니다!

 const ingredientList = useMemo(() => {
    return (
      <IngredientList
        ingredients={userIngredients}
        onRemoveItem={removeIngredientHandler}
      />
    );
  }, [userIngredients, removeIngredientHandler]);
  
  ..
  
  
  <section>
        <Search onLoadIngredients={filteredIngredientsHandler} />
        {ingredientList}
  </section>

마지막으로 최적화하고 싶은 곳은 <ErrorModal 입니다>
onClose에서 clearError함수를 호출합니다! useCsllback을 사용해 ErroeModal이 불필요하게 렌더링하지 않게 합니다.

 const clearError = useCallback(() => {
   dispatchHttp({ type: "CLEAR" });
 },[]);

ErrorModal.js에서는 React.memo를 이용하고있습니다.

useMemo()를 사용하면, 어떤 데이터든 저장하여 컴포넌트가 렌더링 될 떄마다 다시 생성되지 않도록 할 수 있씁니다.

커스텀 훅 시작하기

우리만의 훅을 만드는 기능입니다.
현재 Ingredients.js에서 여러 개의 http요청을 보내고 있기 떄문에
hooks 폴더를 만들고 http.js 파일을 만들겠습니다.

여기에서는 Ingredients.js에서 httpReducer 부분을 옮겨주면 되겠습니다.

  • http.js

    이렇게하면 useHttp를 사용한느 컴포넌트가 다시 실행될 떄 마다 이 HTTP 요청이 전송될 겁니다.
    그래서 sendRequest() 를 만들어 안에 fetch하는 구문을 넣어서 sendRequest()가 호출될 떄 마다 요청 전송을 하게했습니다.

profile
하루하루 기록하기!

0개의 댓글