Dev

hooks로 만드는 reducer

Joo 2019. 5. 6. 10:02

react를 사용하는데 redux는 필수적인 요소라고 할 수 있다. 이제는 다른 방법들도 많은 것 같지만 결국 redux와 같이 app 전체에서 공유하는 공통 state를 사용하는 것은 비슷하다.

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { changeName } from './actions'

@connect(state => ({
	name: state.name
}), dispatch => ({
	handleChangeName: (name) => {
    dispatch(changeName(name))
  }
}))
class MyComponent extends Component {

  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
  }
    
  handleChange(e) {
    const { handleChangeName } = this.props
    handleChangeName(e.target.value)
  }
	
  render() {
    const { name } = this.props
    return (
      <div>
        Hello,
        <input
          type='text'
          value={name}
          onChange={this.handleChange}
        />
      </div>
    )
  }
}

그 동안의 component는 이런 식으로 es 클래스로 만들었는데 react 16.8에서 등장한 hooks 때문에 이 모습은 많이 바뀌었다. 먼저 component를 class 대신 function으로 만드는 것이 기본 형태가 되었다. 내부 method에 bind를 하는 대신 직접 호출할 수 있게 되었고, setState 대신 set으로 직접 값을 변경하는 등 객체지향적인 모습을 많이 벗어버리고 좀 더 javascript 본연의 모습과 가깝게 되었다.

component가 훨씬 간결해지고 직관적으로 변화되었지만 쉽사리 변경하지 못하는 문제가 있었다. 바로 redux다. 위의 코드만 하더라도 decorator를 사용한 connect는 그리 거슬리지 않는다. 하지만 일전에 정리한 functional component에 redux를 적용하는 방법만 하더라도 별로 좋아보이지 않는다. 이왕이면 hooks를 사용하면 좋았을텐데 방법이 없었다.

useReducer 사용

어떻게든 hooks를 사용해서 이 문제를 해결해보고 싶었다. 그래서 useReducer를 사용해봤다. useReducer는 redux에서 제공하는 state, dispatch를 거의 유사하게 제공한다. 하지만 전역으로 사용하기 위해서는 이 state, dispatch를 제공하는 store가 공유되어야 하는데 그게 안된다. 그래서 Context api를 사용해서 해결해보고자 했다.

export const initialState = { ... }
export const AppContext = createContext(initialState);
export function reducer(state = initialState, action) { ... }

export default function(props) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
      <AppContext.Provider value={{state, dispatch}}>
        <div className={classes.root}>
          <Header />
          <Content />
        </div>
      </AppContext.Provider>
  );
}

어떻게 되긴한다. Provider에 state, dispatch를 넘겨주고 Consumer에서 getContext로 이걸 사용하니까 잘된다. 근데 문제는 getContext에 context를 넘겨주기 위해서 만든 AppContext를 export해서 사용해야했고 redux에서 제공하는 combineReducer 같은 기능은 제공하지 않기 때문에 reducer에 많은 데이터를 넣기가 부담이 된다.

useReducer는 아무래도 component 하나에서 state를 효과적으로 관리하는 용도로만 사용해야할 것 같다. 아무래도 state를 컨트롤하는 로직이 component에서 분리될테니 좀 더 간결한 component 생성이 가능할 것이다. 그럼 hooks로 데이터 관리는 포기하고  connect를 사용해야 하느냐 다시 원점으로 돌아온다.

react-redux의 hooks

이전에는 react-redux에서 hooks를 제공하지 않았다. 그런데 7.1.0-alpha 버전에서 hooks에 대한 개발과 테스트가 한창이다. useRedux를 썼다가 useActions를 썼다가 alpha4 버전에서 useSelector, useDispatch로 정리되는 분위기다. 언제 나오나 언제 나오나 했는데 곧 나올 분위기다. 하지만 아직 alpha이고api가 생겼다 없어졌다 마구 바뀌고 있어서 언제 나올지 확신할 수는 없다. 그래도 궁금하니 소소한 프로젝트에 한번 써보기로 했다.

  const user = useSelector(state => state.user)
  const dispatch = useDispatch()

  useEffect(() => {
    axios.get('/api/user')
      .then(res => {
        dispatch(updateUser(res.data.user));
      })
  }, []);

기존에 사용하던 connect 의 첫번째 파라미터는 useSelector로, 두번째 파라미터는 useDispatch로 생각하면 편하다. state의 각 값마다 useSelector를 사용해줘야 한다는 점은 약간 번거로울 수 있지만 그래도 그 편이 더 직관적이다. action을 dispatch할때 주로 한번 감싸서 props에 넣어줬는데 dispatch를 직접 받아서 action을 처리하는 것도 괜찮은 것 같다.

써보니 더 좋은 것 같다. Context api는 redux를 완전히 대체하지 못할 것 같고 언뜻 이해가 안되던 hoc 방식의 connect를 hooks로 바꾸니 알기가 쉬워졌다. 어서 7.1이 정식 릴리즈되면 좋겠다. 이제는 완전히 class를 벗어나서 functional component로 전환할 수 있을 것 같다.

 

반응형