[React] Deep Dive 모던 리액트(10) useContext,useReducer,기타 훅들
FrontEnd/Deep Dive

[React] Deep Dive 모던 리액트(10) useContext,useReducer,기타 훅들

728x90

useContenxt

 

리액트의 Context에 뭔지 알아야 useContext에 대한 올바른 이해가 가능하다.

 

리액트 애플리케이션은 기본적으로 부모컴포넌트와 자식 컴포너트로 이뤄진 트리 구조를 가지고 있다. 따라서 부모가 가진 데이터를 자식에게 전해주고 싶으면 props를 통해서 데이터를 넘겨줘야 한느데 이 거리가 길어질수록 코드가 복잡해지게 된다.

 

 

<A props={something}>
	<B props={something}>
    	<C props ={something}>
        	<D props = {something}>
            </D>
        </C>
    </B>
</A>

 

 

 

이러한 prop 내려주기는 결국 복잡한 코드를 만들게 되고 특히 해당 값을 사용하지 않는 중간단계의 컴포넌트에서도 props가 열려있어야 한다.

 

Context를 사용하면 이러한 명시적 props없이도 하위 컴포넌트 모두에서 원하는 값을 사용할 수 있다.

 

 

import React, { createContext, useContext } from "react";

const Context = createContext<{ hello: string } | undefined>(undefined);

function ChildComponent() {
  const value = useContext(Context);

  return <>{value ? value.hello : ""}</>;
}

const MyComponent = () => {
  return (
    <>
      <Context.Provider value={{ hello: "react" }}>
        <Context.Provider value={{ hello: "javascript" }}>
          <ChildComponent />
        </Context.Provider>
      </Context.Provider>
    </>
  );
};

/* STYLE */

export default MyComponent;

 

useContext는 상위 컴포넌트에서 만들어진 Context를 함수형 컴포넌트에서 사용할 수 있도록 만들어진 훅이다. useContecxt를 사용해서 상위컴포넌트들 어디선가 선언된 <Context.Provider/>에서 제공한 값을 사용할 수 있게 된다. 이때 Provider가 여러개면 가장 가까운 Provider를 가져오게 된다.

 

 

 

useContext로 원하는 값을 얻으려고 할때 이 콘텍스트가 존재하지 않으면 예상치 못한 에러가 발생하 ㄹ수 있다.

 

이런 에러는 useContext내부에서 해당 콘텍스트가 존재하는 환경인지 확인하면 된다. 조금 쉽게 설명하면 콘텍스트가 한번이라도 초기화되어 값을 내려주고 있는지 확인해야 한다.

 

 

import React, { createContext, useContext } from "react";

const MyContext = createContext<{ hello: string } | undefined>(undefined);

function ContextProvider({ children, text }: any) {
  return (
    <MyContext.Provider value={{ hello: text }}>{children}</MyContext.Provider>
  );
}

function useMyContext() {
  const context = useContext(MyContext);
  if (context === undefined) {
    throw new Error("useMyContext는 ContextProvider내부에서만 사용가능");
  }
  return context;
}

function ChildComponent() {
  const { hello } = useMyContext();

  return <>{hello}</>;
}

const MyComponent = () => {
  return (
    <>
      <ContextProvider text="react">
        <ChildComponent></ChildComponent>
      </ContextProvider>
    </>
  );
};

/* STYLE */

export default MyComponent;

 

 

단 useContext를 활용한 컴포넌트는 재활용이 어려워 진다는 점을 염두에 두자. useContext를 선언한다는 것은 Provider에 의존성을 가지고 있게 된다.

 

그렇다면 모든 콘텍스트를 최상의 루트 컴포넌트에 넣는 방법은 어떨까? 이는 불필요한 리소스낭비를 유발한다. 따라서 컨택스트가 미치는 범위는 최소로 만드는 것이 좋다.

 

콘텍스트와 useContext는 상태관리를 위한 API가 절대 아니다. 콘텍스트는 상태를 주입해주는 API이다. 

 

1. 어떤 상태를 기반으로 다른 상태를 만들어냄
2. 이런 상태변화를 최적화 할 수 있어야함

 

상태관리 라이브러리를 위한 두가지 조건을 useContext는 충족시켜주지 못하고 단순히 props 값을 하위로 전달해 줄 뿐이다.

 

쉽게 생각해서

 

A -> B -> C 단계로 컴포넌트의 props를 전달해준다고 생각하고 A와 C에서만 props를 쓴다고 생각해보자. 이경우 useContext를 쓴다고 B컴포넌트가 렌더링안되는 것이 아니다. 

 

따라서 이런 부분에서 최적화하려면 React.memo를 사용해야 한다.

 

useContext 자체로는 렌더링 최적화에 도움이 되지 않는다.

 

 

useReducer

useState의 심화 버전이라고 생각할 수 있다. useState와 비슷한 형태를 띠지만 조금 더 복잡한 상태값을 미리 정의해 놓은 시나리오에 따라 관리할 수 있다.

 

반환값은 useState와 동일하게 길이가 2인 배열이다.

state : 현재 useState가 가진 값
dispatcher. :state를 업데이트 하는 함수

3개의 인수를 가진다.


reducer : useReducer의 기본 action을 정의하는 함수
initialState : useReducer의 초기값
init : 초기값을 지연해서 생성하고 싶을때 사용하는 함수

 

 

아래는 reducer을 활용한 간단한 카운터 예제이다.

import React, { useReducer } from "react";

type State = {
  count: number;
};

type Action = { type: "up" | "down" | "reset"; payload?: State };

function init(count: State): State {
  return count;
}

const initialState: State = { count: 0 };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "up":
      return { count: state.count + 1 };
    case "down":
      return { count: state.count - 1 > 0 ? state.count - 1 : 0 };
    case "reset":
      return init(action.payload || { count: 0 });
    default:
      throw new Error(`Unexpected action type ${action.type}`);
  }
}

const MyComponent = () => {
  const [state, dispatcher] = useReducer(reducer, initialState, init);

  function handleUpButtonClick() {
    dispatcher({ type: "up" });
  }

  function handleDownButtonClick() {
    dispatcher({ type: "down" });
  }

  function handleResetButtonClick() {
    dispatcher({ type: "reset", payload: { count: 1 } });
  }

  return (
    <>
      <h1>{state.count}</h1>
      <button onClick={handleUpButtonClick}>+</button>
      <button onClick={handleDownButtonClick}>-</button>
      <button onClick={handleResetButtonClick}>reset</button>
    </>
  );
};

/* STYLE */

export default MyComponent;

 

물론 이런 간단한 예제같은 경우 reducer를 사용하면 훨씬 복잡하게 보인다.

 

만약 state가 매우 복잡한 객체로 되어있다면 어떻게 될까? state 값에 대한 업데이트를 컴포넌트 밖에 미리 정의해두고 dispatcer를 통해서 이를 업데이트 하는 방법을 정해주는 방식으로 시나리오들을 관리할 수 있을 것이다.

 

또한 이 경우 state를 사용하는 로직과 이를 관리하는 비즈니스 로직의 분리 효과도 기대할 수 있다.

 

(저번 프로젝트때 reducer를 적용해볼껄 그랬다..)

 

 

useState vs useReducer

Preact의 useState코드를 보면 useReducer로 구현이 되어 있다.

 

https://github.com/preactjs/preact/blob/e201caf396f015a453542b7b9d1be6199582e119/hooks/src/index.js#L147-L153

 

/**
 * @param {import('./index').StateUpdater<any>} [initialState]
 */
export function useState(initialState) {
	currentHook = 1;
	return useReducer(invokeOrReturn, initialState);
}

 

 

결국 useReducer나 useState둘다 세부적인 쓰임에만 차이가 있으며 클로저를 활용하여 값을 가둬서 state를 관리한다는 것은 똑같다. 적재적소에 state와 reducer를 선택하도록 하자!

 

useImperativeHandle

 

useImperativeHandle은 실제 개발 과정에서는 자주볼수 없는 훅이나 일부 사례에선 유용할 수 있다. 이 훅을 이해하기 위해서는 먼저 React.forwardRef에 대해 알아보아야 한다.

 

ref는 useRef에서 반환한 객체이며 HTMLElement에 접근하는 용도로 많이 사용된다.

 

이러한 ref를 하위 컴포넌트로 전달하고 싶으면 어떻게 할 수 있을까. 만약 ref를 props로 전달하면 안된다.

 

ref가 예약어이기 떄문에 props로 사용할 수 없다는 것이다.

 

import { useEffect, useRef } from "react";

function ChildComponent({ ref }: any) {
  useEffect(() => {
    console.log(ref); // undefined
  }, [ref]);

  return <div>I'm MINGYU!!1</div>;
}

export default function MyComponent() {
  const inputRef = useRef();

  return (
    <>
      <input ref={inputRef} />
      <ChildComponent ref={inputRef}></ChildComponent>
    </>
  );
}

 

 

물론 아래처럼 ref대신 다른 변수로 props를 받으면 정상적으로 동작하는것처럼 보인다.

 

import { useEffect, useRef } from "react";

function ChildComponent({ minRef }: any) {
  useEffect(() => {
    console.log(minRef);
  }, [minRef]);

  return <div>I'm MINGYU!!1</div>;
}

export default function MyComponent() {
  const inputRef = useRef(null);

  return (
    <>
      <input ref={inputRef} />
      <ChildComponent minRef={inputRef}></ChildComponent>
    </>
  );
}

ref가 잘찍힌다.

 

즉 , 일반적인 props를 활용해서 ref를 전달할 수 있다. forwardRef는 방금 작성한 코드와 동일한 작업을 하는 리액트 API이다. 그렇다면 왜 굳이 이 API가 존재할까?

 

그 이유는 ref를 전달하는데 일관성을 제공하기위해서이다. 

 

import { forwardRef, useEffect, useRef } from "react";

const ChildComponent = forwardRef((props, ref) => {
  useEffect(() => {
    console.log(ref);
  }, [ref]);
  return <div>민규!</div>;
});

export default function MyComponent() {
  const inputRef = useRef(null);

  return (
    <>
      <input ref={inputRef} />
      <ChildComponent ref={inputRef}></ChildComponent>
    </>
  );
}

 

 

위와같이 forwardRef를 사용하면 사용하는쪾에서도 받는 쪽에서도 ref를 받는다는 것을 확실하게 이해할 수 있다.

 

 

useImperativeHandle

해당 훅은 부모에게서 넘겨받은 ref를 원하는 대로 수정할 수 있게 해주는 훅이다.

 

import { forwardRef, useImperativeHandle, useRef, useState } from "react";

const Input = forwardRef((props: any, ref) => {
  useImperativeHandle(ref, () => ({ alert: () => alert(props.value) }), [
    props.value,
  ]);
  return <input ref={ref} {...props} />;
});

export default function MyComponent() {
  const inputRef = useRef();

  const [text, setText] = useState("");

  function handleClick() {
    // @ts-ignore
    inputRef.current.alert();
  }

  function handleChange(e: any) {
    setText(e.target.value);
  }

  return (
    <>
      <Input ref={inputRef} value={text} onChange={handleChange} />
      <button onClick={handleClick}>Focus</button>
    </>
  );
}

 

 

해당 훅을 사용하면 자식컴포넌트에서 새롭게 설정한 객체의 키와 값에 대해서도 접근할 수 있다.

 

useLayoutEffect

 

useEffect와 동일하지만 , 모든 DOM의 변경 후에 동기적으로 발생하게 된다. 즉 사용법 자체는 useEffect와 동일하다.

 

모든 DOM의 변경이란 렌더링을 의미하지 실제로 해당변경이 반영되는 시점은 아니다.

 

1. 리액트가 DOM 업데이트

2. useLayoutEffect를 실행

3. 브라우저에 변경사항 반영

4. useEffect 실행

 

즉 오히려 useEffect보다 실행 속도가 빠르다.단 이 useLayoutEffect는 동기적으로 실행되기때문에 컴포넌트가 일시중지되는것과 같은 일이 발생할 수 있다. 따라서 DOM이 계산되었음에도 화면에 반영되기 전에 할 작업이 있는경우에만 조심스럽게 사용하자.

 

 

useDebugValue

일반적으로 리액트 애플리케이션을 개발하는 과정에서 사용되며, 디버깅하고 싶은 정보를 이 훅에 사용하면 리액트 개발자 도구에서 볼 수 있따.

 

 

 

 

728x90