본문 바로가기

React/실험실

[React] Context Module Function 패턴

// src/context/counter.js
const CounterContext = React.createContext()

function CounterProvider({step = 1, initialCount = 0, ...props}) {
  const [state, dispatch] = React.useReducer(
    (state, action) => {
      const change = action.step ?? step
      switch (action.type) {
        case 'increment': {
          return {...state, count: state.count + change}
        }
        case 'decrement': {
          return {...state, count: state.count - change}
        }
        default: {
          throw new Error(`Unhandled action type: ${action.type}`)
        }
      }
    },
    {count: initialCount},
  )

  const value = [state, dispatch]
  return <CounterContext.Provider value={value} {...props} />
}

function useCounter() {
  const context = React.useContext(CounterContext)
  if (context === undefined) {
    throw new Error(`useCounter must be used within a CounterProvider`)
  }
  return context
}

export {CounterProvider, useCounter}
// src/screens/counter.js
import {useCounter} from 'context/counter'

function Counter() {
  const [state, dispatch] = useCounter()
  const increment = () => dispatch({type: 'increment'})
  const decrement = () => dispatch({type: 'decrement'})
  return (
    <div>
      <div>Current Count: {state.count}</div>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  )
}
// src/index.js
import {CounterProvider} from 'context/counter'
import Counter from 'screens/counter'

function App() {
  return (
    <CounterProvider>
      <Counter />
    </CounterProvider>
  )
}

 

시작부터 긴 코드 덩어리가 나와서 읽기 거북할텐데, 간단하게 정리하면 Context를 사용해서 간단한 Counter를 만들었다. 

Context에서 useReducer를 사용해서 action에따라 +1, -1을 하고 있다. 

 

그런데 Counter 컴포넌트에서 사용하고 있는 Reducer에 집중해보자 

function Counter() {
  const [state, dispatch] = useCounter()
  const increment = () => dispatch({type: 'increment'})
  const decrement = () => dispatch({type: 'decrement'})
  return (
    <div>
      <div>Current Count: {state.count}</div>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  )
}

increment 와 decrement는 dispatch를 호출해서 만들어지고 있는것이 보인다. 

 

생각해보면 이것은 썩 유쾌한 작업이 아니다.

const change = action.step ?? step
switch (action.type) {
  case 'increment': {
    return {...state, count: state.count + change}
  }
  case 'decrement': {
    return {...state, count: state.count - change}
  }
  default: {
    throw new Error(`Unhandled action type: ${action.type}`)
  }
}

이미 Context의 useReducer에서 increment와 decrement에 대한 작업을 모두 선언했는데, 실제로 사용하기 위해서 

dispatch를 호출하는 함수를 매번 만들어주는 작업은 생각보다 많이 귀찮은 작업이다. 

 

어떤 방법을 사용하면 이 작업을 쉽게 해결할 수 있을까? 

 

"Helper" 함수를 만들고 Context를 포함시키기

function CounterProvider({step = 1, initialCount = 0, ...props}) {
  const [state, dispatch] = React.useReducer(
    // ...
  )
  
  const increment = React.useCallback(
    () => dispatch({type: 'increment'}),
    [dispatch],
  )
  const decrement = React.useCallback(
    () => dispatch({type: 'decrement'}),
    [dispatch],
  )

  const value = {state, increment, decrement}
  return <CounterContext.Provider value={value} {...props} />
}

useCallback 의 의존성 배열에 dispatch를 추가하고 increment와 decrement를 만들어서 제공해주는 방식이다. 

 

Dispatch를 사용하기

Helper 함수를 만드는 방식도 나쁜 방식은 아니다. 하지만 Dan이라는 사람은 다음과 같이 말했다. 

Helper 메서드는 쓰레기 객체이다. 
보기에는 좋은 구문일지 몰라도 특별한 목적없이 다시 만들고 비교해야 한다. 

개인적으로는 워딩이 쌔다고 느끼지만 결국 Dan이라는 사람이 하고 싶은 말은 Helper 메서드를 사용하기 보다는 

원래 가지고 있는 Dispatch를 사용하는 것을 권장하는 것이다. 

 

function CounterProvider({step = 1, initialCount = 0, ...props}) {
  // ...
  
  return <CounterContext.Provider value={value} {...props} />
}

function useCounter() {
  // ...
}

const increment = dispatch => dispatch({type: 'increment'})
const decrement = dispatch => dispatch({type: 'decrement'})

export {CounterProvider, useCounter, increment, decrement}

함수를 useCallback을 사용하지 않고 dispatch를 파라미터로 제공하는 함수를 export 한다. 

 

다음과 같이 작업하면 Helper 메서드를 만들었을 때처럼 코드의 중복을 줄일 수 있고,

useCallback의 메모이제이션의 의존성 배열의 실수를 방지하고 성능 이슈를 피할 수 있다. 

 

import {useCounter, increment, decrement} from 'context/counter'

function Counter() {
  const [state, dispatch] = useCounter()
  return (
    <div>
      <div>Current Count: {state.count}</div>
      <button onClick={() => decrement(dispatch)}>-</button>
      <button onClick={() => increment(dispatch)}>+</button>
    </div>
  )
}

Redux 등을 사용하지 않고 Context를 사용하는 경우 도움이 될 것 같다. 

 

출처 

https://advanced-react-patterns.netlify.app/1

 

Advanced React Patterns 🤯

 

advanced-react-patterns.netlify.app

 

반응형

'React > 실험실' 카테고리의 다른 글

[React] redux-persist  (1) 2023.02.11
[React] Compound Component 패턴  (1) 2023.02.04
[React] Context API 언제 사용해야할까?  (0) 2023.01.31
[React] 독립된 React 컴포넌트의 이점  (0) 2023.01.29
[React] Suspense  (0) 2023.01.28