React 알아가기 (6)

삔아·2023년 5월 9일
0
post-thumbnail

해당 내용은 https://www.udemy.com/course/best-react/ 강의를 들으며 정리하고 스스로 공부 한 내용을 기록 하였습니다.

Section 5. 고급 리듀서를 사용하여 부작용 처리 & 컨텍스트 API 사용하기

About useReducer()

useReducer() 훅은 useState 훅과 약간 비슷하다.
이 역시 리액트에 내장된 훅으로 useState 보다는 조금 더 강력한 기능을 지원하기 때문에 여러 state들이 함께 속해 있을 때, 여러 state가 같이 바뀌거나 서로 관련된 경우 일 때 등의 복잡한 상황일 때 종종 쓰인다.
그렇다고 더 많은 기능을 지원한다고 해서 매번 useReducer() 쓰는 것은 좋지가 않다. 이 훅을 사용 하기 위해 설정 하는 부분도 복잡할 뿐 더러 간단한 useState 훅 보다는 더 복잡하기 때문이다.

다른 state 를 기반으로 하는 state를 업데이트 하게 되는 경우에는 하나의 state 로 병합 해도 좋다.

useReducer() 사용하기

const [state, dispatchFn] = useReducer(reducerFn, initialState, initFn);
  1. state: state 스냅샷
  2. dispatchFn: state를 업데이트 하는 함수 -> 액션을 dispatch 하는 점에 주목
  3. reducerFn: 최근 state 스냅샷을 자동으로 가져오는 함수,
    이 함수는 디스패치 된 액션을 가져온다. 리액트는 새 액션이 디스패치 될 때 마다 해당 함수를 호출한다. 리듀서 함수 실행을 트리거하는 디스패치 된 액션을 가져오며 새로 업데이트된 state를 반환한다.
  4. initialState: 초기 state
  5. initFn: 초기 state를 설정하는 함수 (http리퀘스트 결과가 필요한 경우에 쓰인다.)

예시를 통해 해당 부분에 대해 좀 더 확인해보도록 하자.
해당 예시는 강의의 코드 중 하나로 로그인 사이트에서 email의 value (이메일 입력 값), valid (유효성) 를 병합하여 useReducer 로 사용한 부분이다.

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

// 컴포넌트 내부에 쓰이는 어떤 데이터도 필요하지 않기 때문에 컴포넌트 함수 외부에 작성 가능
const emailReducer = (state, action) => {
  if (action.type === "USER_INPUT") {
    return { value: action.val, isValid: action.val.includes("@") };
  }
  if (action.type === "INPUT_BLUR") {
    // state 는 늘 최신의 스냅샷
    return { value: state.value, isValid: state.value.includes("@") };
  }

  return { value: "", isValid: false };
};

const Login = (props) => {
  // const [enteredEmail, setEnteredEmail] = useState("");
  // const [emailIsValid, setEmailIsValid] = useState();
  ...

  const [emailState, dispatchEmail] = useReducer(emailReducer, {
    value: "",
    isValid: null,
  });

  const emailChangeHandler = (event) => {
    // 보통 객체로 전달 또한 해당 부분은 페이로드가 필요할테니 추가 (=val)
    dispatchEmail({ type: "USER_INPUT", val: event.target.value });
	...
  };
	
    ...
    
  const validateEmailHandler = () => {
    dispatchEmail({ type: "INPUT_BLUR" });
  };

  return (
    <Card className={classes.login}>
      <form onSubmit={submitHandler}>
        <div
          className={`${classes.control} ${
            emailState.isValid === false ? classes.invalid : ""
          }`}
        >
          <label htmlFor="email">E-Mail</label>
          <input
            type="email"
            id="email"
            value={emailState.value}
            onChange={emailChangeHandler}
            onBlur={validateEmailHandler}
          />
        </div>
        ...
      </form>
    </Card>
  );
};

export default Login;

useState() vs useReducer()

위에서도 언급했지만 useReducer 훅이 useState보다 강력한 기능을 지원한다고 해서 많이 쓰는 것은 좋지 않다.
그럼 언제 useState 를 쓰고 언제 useRedcuer 를 쓰면 될까?

너무 많은 일들을 처리해야 하는 경우 혹은 관련 state 스냅샷 들이 서로 독립적이고 같이 업데이트가 잘 안된다면 그 경우에는 useReducer 를 선호한다.

useState()

  • 주요 state 관리 도구
  • 개별 state 및 데이터들을 다루기에 적합
  • 간단한 state에 적합하며 state 업데이트가 쉬우며 몇 종류 안되는 경우에 적합

useReducer()

  • 만약 state 로서의 객체가 있는 경우 또는 복잡한 state가 있는 경우에 적합
  • 복잡한 state 업데이트 로직을 포함하는 리듀서 함수를 사용 할 수 있다.
  • 최신 state 스냅샷 또한 그 복잡할 수도 있는 로직을 컴포넌트 함수에서 별도의 리듀서 함수로 이동 시킬 수 있다.
  • 연관된 state 조각들로 구성되었으며 해당 state 관련 데이터를 다루는 경우에 적합하다.

항상 useRedcuer 훅을 써야 하는 것은 아니다.
두 개의 서로 다른 값을 전환하기만 하는 단순한 state가 있는 경우 useReducer 를 쓰기엔 너무 과하다.


About Context API

앱이 커지면 커질수록 컴포넌트가 많아지며 컴포넌트간에 데이터를 주고받기 위해 부모 컴포넌트에 데이터를 보내주고 받아와야하는데, 이 과정이 매우 복잡해져 불편해질 수 있다.

즉, 실제로 필요한 데이터를 부모를 통해서 받는 것이 아니라 해당 컴포넌트에서 바로 사용하길 원하는데 이를 해소하기 위해 컴포넌트 전체에서 사용 할 수 있는 즉 내부적인 state 저장소가 있다. 바로 React Context 이다.

React Context API 이용하기

context 를 담아두는 폴더는 보통 src 하위 폴더에 두며 케밥 케이스를 이용한다.

  1. React.createContext() 를 호출한다.
  • 컨텍스트 객체 생성하는 부분
  • 컨텍스트는 앱이나 빈 state의 컴포넌트 일 뿐 기본 state로 그냥 텍스트를 써도 되나 대부분 객체형을 띄고있다.
const AuthContext = React.createContext({
  isLoggedIn: false
});
  1. 어떤 컴포넌트에서든 접근이 가능 하도록 공급 및 소비(연동, 리스닝) 가능하도록 리액트에게 Context가 있음을 알린다.
    공급 한다 는 것은 JSX코드로 감싼다는 것을 의미한다. 감싸져야 리스닝 또한 가능하다.

// App.js (최상위 부모)
return (
    <Fragment>
      <MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
      <main>
        {!isLoggedIn && <Login onLogin={loginHandler} />}
        {isLoggedIn && <Home onLogout={logoutHandler} />}
      </main>
    </Fragment>
  );

예시의 코드는 작은 규모를 가지고 있지만.. 우리는 이 isLoggedIn 이 어디에서나 다 필요하다는 것을 확인 할 수 있다.

이 때, <Fragment> 아래에 위에 생성해준 AuthContext 로 감싸준다.
또한 ‘공급’ 한다는 것을 알려야 하기 때문에 최종적으로 <AuthContext.Provider> 로 감싸주는 형태가 된다.
Wrapper 컴포넌트의 기능을 하기 때문에 React.Fragment 의 기능을 할 수 있다. (해당 태그를 지워도 된다는 뜻)

이제 모든 컴포넌트는 해당 컨텍스트에 접근 할 수 있다.

  1. 리스닝 전달 방법은 두가지가 있다.
  • context 소비자
  • 컨텍스트 훅 이용

Consumer 를 이용할 때 아래에 자식 부분이 있다.

{(ctx) => { return jsx code}}

여기서 ctx는 해당 Context의 data를 의미한다.

import React from 'react';

import classes from './Navigation.module.css';
import AuthContext from '../../store/auth-context';

const Navigation = (props) => {
  return (
		// 해당 컨텍스트 데이터가 필요한 곳에 호출
    <AuthContext.Consumer>
      {(ctx) => {        
        return //기존 JSX코드
		<nav className={classes.nav}>
      <ul>
		// props.isLoggedIn -> ctx.isLoggedIn 에 접근 가능
        {ctx.isLoggedIn && (
          <li>
            <a href="/">Users</a>
          </li>
        )}
        {ctx.isLoggedIn && (
          <li>
            <a href="/">Admin</a>
          </li>
        )}
        ...
      </ul>
    </nav>
      }}
    </AuthContext.Consumer>
  );
};

export default Navigation;

💡 기본 값이 있으면 공급자는 필요 없다.

지금 같은 상황에 AuthContext 에는 기본 값을 가지고 있다.

const AuthContext = React.createContext({
  isLoggedIn: false
});

이렇게 기본 값을 가지고 있다면 Provider 가 필요가 없다.
하지만 현재는 연습하는 단계이므로 Provider를 사용해보자.

<AuthContext.Provider
  value={{
    isLoggedIn: false,
  }}
 >

이렇게 작성하면 반응형이 되지 않으므로 상황에 맞추어 반응형으로 되도록 작성해준다.

<AuthContext.Provider
  value={{
    isLoggedIn: isLoggedIn,
  }}
>

해당 예시에서는 isLoggedIn 이라는 state를 컴포넌트가 가지고 있기 때문에 이를 이용했다.

이렇게 되면 isLoggedIn 이 변경될 때 마다 리액트에 의해 업데이트 된다. 그렇게 되면 새로운 컨텍스트 객체는 모든 리스닝 컴포넌트로 전달이 된다.
해당 컨텍스트를 소비하는 모든 컴포넌트에 전달 된다.

해당 방법은 함수를 만들어야하고 JSX코드를 반환해야하고 (consumer에서..) 코드가 별로 예쁘지 않다.
해당 방법에 대한 대안 방법이 있다. 바로 컨텍스트 훅을 이용 하는 것.

useContext() 훅 이용하기

컨텍스트에게 사용하려는 컨텍스트를 가리키는 포인터 전달한다.

const ctx = useContext(AuthContext);

예시)

const Navigation = (props) => {
  const ctx = useContext(AuthContext);

  return (
    <nav className={classes.nav}>
      <ul>
        {ctx.isLoggedIn && (
          <li>
            <a href="/">Users</a>
          </li>
        )}
        {ctx.isLoggedIn && (
          <li>
            <a href="/">Admin</a>
          </li>
        )}
        {ctx.isLoggedIn && (
          <li>
            <button onClick={props.onLogout}>Logout</button>
          </li>
        )}
      </ul>
    </nav>
  );
};

context는 단순 값 뿐 만 아니라 함수도 전달이 가능하다.

// App.js
const logoutHandler = () => {
    localStorage.removeItem("isLoggedIn");
    setIsLoggedIn(false);
  };

return (
  <AuthContext.Provider
    value={{
      isLoggedIn: isLoggedIn,
        onLogout: logoutHandler,
    }}
  >
);

이처럼 함수의 포인터를 가리키면 된다. 이렇게 작성해주면 AuthContext를 리스닝하는 모든 컴포넌트는 logoutHandler를 활용할 수 있다.

대부분의 경우에는 props를 사용하여 컴포넌트에 데이터를 전달한다.
props는 컴포넌트를 구성하고 그것들을 재사용할 수 있도록 하는 매커니즘이기 때문이다.

만약 많은 컴포넌트를 통해 전달하고자 하는 것이 있는 경우, 예를 들어 네비게이션처럼 매우 특정적인 일을 하는 컴포넌트나 항상 사용자를 로그아웃시키는 버튼 등의 여러 컴포넌트로 전달하는 경우에만 컨텍스트를 사용하는 게 더 나을 것이다.

React Context 제한

컴포넌트 구성의 변경이 잦은 경우에는 적합하지 않다.

예를 들어 매초 또는 1초에 여러번 state가 변경되는 경우에 리액트 컨텍스트는 이에 대해 최적화 되어 있지 않다.
만약 앱 전체에 걸쳐 또는 컴포넌트 전체에 걸쳐 state가 자주 변경되는 경우에, 컨텍스트를 사용하고 싶은데 props는 적합하지 않을 떄 Redux를 사용한다.


⭐ Hooks의 규칙

  1. 리액트 훅은 리액트 함수 컴포넌트 또는 사용자 정의 훅에서만 사용 할 수 있다.
  2. 리액트 훅은 리액트 컴포넌트 함수 또는 사용자 정의 훅 함수의 최상위 수준에서만 호출해야 한다.
    -> 중첩 함수 혹은 block문에서 호출 하면 안된다.
  3. useEffect() 훅에서 참조하는 모든 항목 들 중 컴포넌트 함수 외부에서 오는 데이터들은 의존성 배열에 추가해야 한다.
    -> useReducer() 혹은 useState() 에 의해 노출된 state업데이트 함수는 변경되지 않도록 리액트가 보장하기 때문에 의존성으로 추가 할 필요는 없다.
profile
Frontend 개발자 입니다, 피드백은 언제나 환영 입니다

0개의 댓글