모던 리액트 Deep Dive 스터디

리액트 훅 깊게 살펴보기(useReducer, useRef)

려낭 2024. 11. 15. 22:07

3.1.5 useRef

 

useRef와 useState는 동일하게 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다는 공통점이 있다.

 

useState와 구별되는 큰 차이점 두 가지

  • useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.
  • useRef는 그 값이 변하더라도 렌더링을 발생시키지 않는다.

useRef로 useState를 흉내내도 렌더링이 되지 않는다.

 

 

useRef가 왜 필요할까?

 

useRef는 컴포넌트가 렌더링될 때만 생성되고, 컴포넌트 인스턴스가 여러 개라도 각각 별개의 값을 바라본다.

 

useRef의 가장 일반적인 사용 예는 DOM에 접근하고 싶을 때 이다.

function RefComponent () {
	const inputRef = useRef()
    
    //이때는 렌더링이 실행되기 전(반환되기 전)이므로 undefined를 반환한다.
    console.log(inputRef.current) //undefined
    
  useEffect(() => {
    console.log(inputRef.current) //<input type="text"></input>
 } , [inputRef])
 
 return <input ref={inputRef} type="text" />

 

useRef는 최초에 넘겨받은 기본값을 가지고 있다.

useRef의 최초 기본값은 return문에 정의해 둔 DOM이 아니라 useRef() 로 넘겨받은 인수이다.

useRef가 선언된 당시에는 아직 컴포넌트가 렌더링되기 전이라 return으로 컴포넌트의 DOM이 반환되기 전이기 때문에 undefined

 

원하는 시점의 값을 렌더링에 영향을 미치지 않고 보관해 두고 싶다면 useRef를 사용하는 것이 좋다.

 

 

 

3.1.6 useContext

 

useContext에 대해 이해하려면 리액트의 Context에 대해 알아야 한다.

 

Context란?

 

props drilling : A 컴포넌트에서 제공하는 데이터를 D 컴포넌트에서 사용하려면 props를 하위 컴포넌트로 필요한 위치까지 계속해서 넘기는 기법.

 

prop 내려주기의 번거로운 작업을 극복하기 위해 등장한 개념이 바로 콘텍스트(context)이다.

 

콘텍스트를 사용하면, 이런 명시적인 props 전달 없이도 선언한 하위 컴포넌트 모두에서 자유롭게 원하는 값을 사용할 수 있다.

 

 

Context를 함수 컴포넌트에서 사용할 수 있게 해주는 useContext 훅

 

useContext는 상위 컴포넌트에서 만들어진 Context를 함수 컴포넌트에서 사용할 수 있도록 만들어진 훅이다.

 

useContext를 사용하면 상위 컴포넌트 어딘가에서 선언된 <Context.Provider /> 에서 제공한 값을 사용할 수 있게 된다.

여러개의 Provider가 있다면 가장 가까운 Provider의 값을 가져오게 된다.

 

다수의 Provider와 useContext를 사용할 때, 특히 ts 사용중이라면 별도 함수로 감싸서 사용하는 것이 좋다.

> 타입 추론에도 유용하고, 상위에 Provider가 없는 경우 사전에 쉽게 에러를 찾을 수 있다.

 

useContext를 사용할 때 주의할 점

 

useContext를 함수 컴포넌트 내부에서 사용할 때 컴포넌트 재활용이 어렵다.

 

왜?

 

useContext가 선언돼 있으면 Provider에 의존성을 가지고 있는 셈이 되기 때문.

꼭 해당 함수 컴포넌트가 Provider 하위에 있는 상태에서 useContext를 사용해야 한다.

 

useContext로 상태 주입을 최적화했다면 반드시 Provider의 값이 변경될 때 어떤 식으로 렌더링되는지 확인해야한다.

useContext로는 주입된 상태를 사용할 수 있을 분, 그 자체로는 렌더링 최적화에 아무 도움이 되지 않는다.

 

 

3.1.7 useReducer

 

useReducer은 useState의 심화 번으로 볼 수 있다.

useState와 비슷한 형태를 띠지만 좀 더 복잡한 상태값을 미리 정해놓은 시나리오에 따라 관리할 수 있다.

 

useReducer에서 사용되는 용어

  • 반환값은 useState와 동일하게 길이가 2인 배열이다.
    • state: 현재 useReducer가 가지고 있는 값. useState와 마찬가지로 배열을 반환하는데, 동일하게 첫 번째 요소가 이 값이다.
    • dispatcher: state를 업데이트하는 함수. useReducer가 반환하는 배열의 두 번째 요소다. setState는 단순히 값을 넘겨주지만 여기서는 action을 넘겨준다는 점이 다르다. 이 action은 state를 변경할 수 있는 액션을 의미한다.
  • useState의 인수와 달리 2개에서 3개의 인수를 필요로 한다.
    • reducer: useReducer의 기본 action을 정의하는 함수다. 이 reducer는 useReducer의 첫 번째 인수로 넘겨주어야 한다.
    • initialState: 두 번째 인수로, useReducer의 초깃값을 의미한다.
    • init: useState의 인수로 함수를 넘겨줄 때처럼 초깃값을 지연해서 생성시키고 싶을 때 사용하는 함수다. 이 함수는 필수값이 아니며, 만약 여기에 인수로 넘겨주는 함수가 존재한다면 useState와 동일하게 게으른 초기화가 일어나며 initialState를 인수로 init 함수가 실행된다.
// useReducer가 사용할 상태의 타입 정의
type State = {
  count: number
}

// state의 변경을 발생시킬 action의 타입과 넘겨줄 값 (payload) 정의
// 각 type 값에 payload를 넣어줄 지 말지 모드 모양이 달라질 필요도 없다.
// 다만 이름만 네이밍이 가장 많이 달려 쓰인다.
type Action = { type: 'up' | 'down' | 'reset', payload?: State }

// 무거운 연산이 포함된 경우 초기값을 초기화할 함수
function init(count: State): State {
  return count
}

// 초기값
const initialState: State = { count: 0 }

// 앞서 선언한 state와 action을 기반으로 state가 어떻게 변경될지 정의
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}`)
  }
}

export default function App() {
  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 } })
  }

...

 

useReducer의 목적은 복잡한 형태의 state를 사전에 정의된 dispatcher로만 수정할 수 있게 만들어 줌으로써 state값에 대한 접근은 컴포넌트에서만 가능하게 하고, 이를 업데이트 하는 방법에 대한 상세 정의는 컴포넌트 밖에다 둔 다음, state의 업데이트를 미리 정의해 둔 dispatcher로만 제한하는 것이다.

state 값을 변경하는 시나리오를 제한적으로 두고 이에 대한 변경을 빠르게 확인할 수 있게끔 하는 것이 useReducer의 목적이다.