정말 간만의 포스트입니다. 요새 더 멋진 velog 를 개발하느라.. 통 글을 못 쓰고 있었네요! 오늘 준비한 포스트에서는 react-redux 에서 Hooks 를 사용하는 방법에 대해서 알아보도록 하겠습니다.
이번에, 튜토리얼과 함께 영상도 만들어보았습니다... 튜토리얼 진행 과정을 그대로 따라해보고 싶으신분은 위 영상과 함께 진행하시면 도움이 될거예요.
우선, 리액트에 도입된 Hooks 에 대해서 잘 모르시는 분들은 Hooks 완벽 정복하기 포스트를 읽어주세요.
이 포스트는 여러분이 리덕스에 대해서 이미 잘 알고있다는 가정 하에 튜토리얼을 진행하게 됩니다. 만약에 리덕스를 잘 모르신다면 리덕스 시리즈를 먼저 읽어주세요.
Hooks 서포트는 react-redux v7.1 부터 지원이 되는 기능입니다. 이 기능은 현재 alpha 상태이며 아직까지는 정식으로 릴리즈되지 않았습니다. 만약에 변동이 생기거나, 정식적으로 릴리즈 되면 이 포스트에서도 반영하도록 하겠습니다.
현재 useActions 랑 useRedux 가 사라졌습니다.
react-redux 의 Hooks 에 관련한 공식 문서는 여기에 있습니다.
이 포스트에서는 리액트 프로젝트를 생성해서 리덕스를 사용하여 카운터와 Todo List 를 만들어보게 됩니다. 우리는 살면서 과연 개발 공부를 하게 되면서 카운터는 몇번이나 만드는거고, Todo List 는 대체 몇번이나 만들게 되는 걸까요? ㅎㅎ
시간을 단축하기 위하여 우리는 CRA 를 사용하여 프로젝트를 구성하도록 하겠습니다.
$ yarn create react-app react-redux-hooks-tutorial
그리고, 해당 프로젝트 디렉터리에 redux, react-redux 를 설치하세요. react-redux 를 설치 할 때에는 @next 태그를 붙여주세요.
만약 제가 이 포스트 수정을 조금 늦게 했다면, 여러분이 이 포스트를 보는 시점에 이미 정식 릴리즈가 되었을지도 모릅니다. 이 링크 를 확인하여 Hooks 가 정식 릴리즈로 탑재가 되었는지 확인해보세요.
$ yarn add redux react-redux@next redux-devtools-extension
그 다음엔 src 디렉터리에 다음 디렉터리를 생성하세요:
우리는 ducks 패턴을 사용해서 액션 / 액션 생성 함수 / 리듀서가 한 파일에 들어있는 리덕스 모듈을 작성 할 것입니다.
가장 먼저, 카운터를 구현해봅시다. 우선 리덕스 모듈부터 만들어보세요.
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';
export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });
const initialState = 0;
const counter = (state = initialState, action) => {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
default:
return state;
}
};
export default counter;
그 다음엔, 루트 리듀서를 만드세요. 물론 지금은 리듀서가 하나 뿐이지만 추후 더 만들 것 입니다.
import { combineReducers } from 'redux';
import counter from './counter';
const rootReducer = combineReducers({
counter
});
export default rootReducer;
그리고 나서 프로젝트의 엔트리 파일 index.js 에서 스토어를 만들고 Provider 를 통하여 프로젝트에 리덕스를 적용하세요.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from './modules';
import { Provider } from 'react-redux';
const store = createStore(rootReducer, composeWithDevTools());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
현재 리덕스 개발자 도구도 적용을 해주었는데요, 만약에 아직 크롬 확장 프로그램을 설치하지 않으셨으면 여기서 설치하세요.
이제 카운터의 프리젠테이셔널 컴포넌트를 만드세요.
import React from 'react';
const Counter = ({ onIncrease, onDecrease, number }) => {
return (
<div>
<h1>{number}</h1>
<div>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
</div>
);
};
export default Counter;
이제 컨테이너를 만들어봅시다. 드디어 Hooks 를 사용 할 차례입니다! 기대가 되지않나요?
가장 먼저 알아볼 Hook 은 useSelector
입니다. 이 Hook 을 통하여 우리는 리덕스 스토어의 상태에 접근 할 수 있습니다.
useSelector 는 다음과 같이 사용합니다.
const result : any = useSelector(selector : Function, deps : any[])
여기서 selector 는 우리가 기존에 connect 로 사용 할 때 mapStateToProps 와 비슷하다고 생각하시면 됩니다. deps 배열은 어떤 값이 바뀌었을 때 selector 를 재정의 할 지 설정해줍니다. deps 값을 생략 하시면 매번 렌더링 될 때마다 selector 함수도 새로 정의됩니다. 기존의 useCallback 이나 useMemo 에서의 deps 랑 동일하다고 보시면 됩니다. 결국, 코드를 뜯어보면 useSelector 도 내부적으로 useMemo 를 사용하고 있답니다 (참고).
selector 함수를 선언하는게 큰 리소스는 들어가진 않기 때문에 기본적으로는 deps 를 넣지 않아도 큰 문제는 없습니다. 그런데 최적화에 신경이 쓰인다면 작업하실 때 두번째 파라미터로 []
를 기본적으로 넣는 것도 괜찮을 것 같습니다. 그리고 실제 dep 배열에 넣어야 되는 값이 보인다면, 그걸 넣으면 더욱 좋겠죠.
이 Hook 을 우리 컴포넌트에서 사용한다면 이렇게 사용하면 됩니다.
const counter = useSelector(state => state.counter, []);
만약에 값 하나만 가져오는게 아니라면 이렇게 할 수도 있겠죠?
const { a, b } = useSelector(state => ({ a: state.a, state.b }), [])
그 다음에는 useActions 를 알아봅시다. 이 Hook 은 기존의 mapDispatchToProps 랑 조금 다릅니다. mapDispatchToProps 는 dispatch 를 파라미터로 가져오는 반면 여기서의 actionCreator 는 그렇지 않습니다.
const boundAC = useActions(actionCreator : Function, deps : any[])
const boundACsObject = useActions(actionCreators : Object<string, Function>, deps : any[])
const boundACsArray = useActions(actionCreators : Function[], deps : any[])
여기서의 deps 는 역시 useSelector 에서 언급한것과 비슷합니다. 생략하셔도 상관은 없습니다만 최적화에 신경쓰신다면 빈 배열을 넣으시거나, 실제로 관계 있는 값을 deps 안에 넣으세요.
위 방식 중에서 boundAC 방식은 액션 생성함수 하나만 사용 할 때 사용합니다.
const onIncrease = useActions(increment)
boundACsObject 는 여러개의 액션 생성함수를 사용 할 때 사용합니다.
const { onIncrease, onDecrease } = useActions({
onIncrease: increment,
onDecrease: decrement
});
마지막으로 onACsArray 도 여러개의 액션 생성함수를 사용 할 때 사용 할 수 있는데, 배열 형태로 반환합니다.
const [onIncrease, onDecrease] = useActions([increase, decrease]);
~와우.. 깔끔쓰...~
그럼 방금 배운것들을 활용해서 컨테이너를 구현해봅시다.
import React from 'react';
import { useSelector, useActions } from 'react-redux';
import Counter from '../components/Counter';
import { increment, decrement } from '../modules/counter';
const CounterContainer = () => {
const counter = useSelector(state => state.counter, []);
const [onIncrease, onDecrease] = useActions([increment, decrement], []);
return (
<Counter number={counter} onIncrease={onIncrease} onDecrease={onDecrease} />
);
};
export default CounterContainer;
이제 이 컴포넌트를 App 에서 렌더링하세요.
import React from 'react';
import CounterContainer from './containers/CounterContainer';
const App = () => {
return <CounterContainer />;
};
export default App;
잘 작동하나요?
이번에는 Todo List 를 만들어보면서 Redux 의 Hooks 를 더 알아봅시다. 우선 Todo List 를 만들기 위한 todos 리듀서를 만들어주세요.
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE_CHECK = 'todos/TOGGLE_CHECK';
const REMOVE = 'todos/REMOVE';
let id = 0;
export const changeInput = input => ({ type: CHANGE_INPUT, payload: input });
export const insert = text => ({
type: INSERT,
payload: {
id: ++id,
text
}
});
export const toggleCheck = id => ({ type: TOGGLE_CHECK, payload: id });
export const remove = id => ({ type: REMOVE, payload: id });
const initialState = {
input: '',
todos: []
};
const todos = (state = initialState, action) => {
switch (action.type) {
case CHANGE_INPUT:
return {
...state,
input: action.payload
};
case INSERT:
return {
...state,
todos: state.todos.concat({ ...action.payload, done: false })
};
case TOGGLE_CHECK:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? {
...todo,
done: !todo.done
}
: todo
)
};
case REMOVE:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
default:
return state;
}
};
export default todos;
그 다음엔, 루트 리듀서에 등록해야겠죠?
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';
const rootReducer = combineReducers({
counter,
todos
});
export default rootReducer;
그리고, TodoList 를 위한 컴포넌트를 만들어줍시다. 편의상 한 파일에 다 만들어보도록 하겠습니다. 이 컴포넌트를 만드는 과정에서 React.memo
를 사용합니다. 꼭 사용해하는건 아니지만, 업데이트 성능 최적화를 하기 위함입니다.
TodoList 컴포넌트는 input 값과 todos 배열을 props 로 받아오게 되는데, input 이 바뀔 때마다 모든 Todo 항목들이 리렌더링 되는건 비효율적이니 이러한 구조에서는 React.memo 를 사용하여 컴포넌트를 구성하는것을 권장드립니다. 물론, 이렇게 조그마한 앱에서는 최적화 안해도 이런걸로 렉이 걸리지는 않습니다.
import React from 'react';
const TodoItem = React.memo(({ todo, onRemove, onToggle }) => {
const { id, text, done } = todo;
return (
<li style={{ textDecoration: done ? 'line-through' : 'none' }}>
<span onClick={() => onToggle(id)}>{text}</span>{' '}
<button onClick={() => onRemove(id)}>삭제</button>
</li>
);
});
const TodoItems = React.memo(({ todos, onRemove, onToggle }) =>
todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onRemove={onRemove}
onToggle={onToggle}
/>
))
);
const TodoList = ({ todos, input, onRemove, onToggle, onChange, onSubmit }) => {
return (
<div>
<form onSubmit={onSubmit}>
<input value={input} onChange={onChange} />
<button type="submit">추가</button>
</form>
<ul>
<TodoItems todos={todos} onRemove={onRemove} onToggle={onToggle} />
</ul>
</div>
);
};
export default TodoList;
이번에 알아볼 Hooks 는 useRedux 입니다. 이 Hook 은 useSelector 와 useActions 의 혼합체입니다. 참고로, 아직 이 기능이 완전히 정착된것 까지는 아니지만, useRedux 는 deps 를 넣을 수 없고, 사용했을때 코드가 장황하다는(?) 측면에서 이는 불필요한 Hook 이라는 피드백도 올라오고 있습니다 (참고 [1] [2]). 과연 이 Hook 은 알파 이후에도 남아있을지는 모르겠지만, 한번 사용법을 알아봅시다.
이 Hook 의 사용법은 다음과 같습니다.
const [selectedValue, boundACs] = useRedux(selector, actionCreators)
만약 우리가 CounterContainer 에서 useRedux 를 사용했더라면 이렇게 사용 할 수 있습니다.
const [counter, [onIncrease, onDecrease]] = useRedux(state => state.counter, [
increment,
decrement
]);
한번 이를 사용하여 컨테이너를 구현해봅시다.
import React, { useCallback } from 'react';
import { useRedux } from 'react-redux';
import { changeInput, insert, toggleCheck, remove } from '../modules/todos';
import TodoList from '../components/TodoList';
const TodoListContainer = () => {
const [
{ input, todos },
[onChangeInput, onInsert, onToggle, onRemove]
] = useRedux(
state => ({
input: state.todos.input,
todos: state.todos.todos
}),
[changeInput, insert, toggleCheck, remove]
);
const onChange = useCallback(
e => {
onChangeInput(e.target.value);
},
[onChangeInput]
);
const onSubmit = useCallback(
e => {
e.preventDefault();
onInsert(input);
onChangeInput('');
},
[input, onChangeInput, onInsert]
);
return (
<TodoList
input={input}
todos={todos}
onChange={onChange}
onSubmit={onSubmit}
onToggle={onToggle}
onRemove={onRemove}
/>
);
};
export default TodoListContainer;
이제 기존에 App 에서 렌더링하고있던 CounterContainer 를 지우고, TodosListContainer 를 렌더링하세요.
import React from 'react';
import TodoListContainer from './containers/TodoListContainer';
const App = () => {
return <TodoListContainer />;
};
export default App;
투두리스트가 잘 작동하나요?
그런데, 아까 언급했다시피 useRedux 는 부정적인 피드백을 많이 받고있으니, useSelector / useActions 로 다시 구현해주겠습니다.
import React, { useCallback } from 'react';
import { useSelector, useActions } from 'react-redux';
import { changeInput, insert, toggleCheck, remove } from '../modules/todos';
import TodoList from '../components/TodoList';
const TodoListContainer = () => {
// todos 리듀서에서 관리하는 객체를 통째로 가져올 거라면 state.todos 로 간소화 시킬 수 있습니다.
const { input, todos } = useSelector(state => state.todos, []);
const [onChangeInput, onInsert, onToggle, onRemove] = useActions(
[changeInput, insert, toggleCheck, remove],
[]
);
const onChange = useCallback(
e => {
onChangeInput(e.target.value);
},
[onChangeInput]
);
const onSubmit = useCallback(
e => {
e.preventDefault();
onInsert(input);
onChangeInput('');
},
[input, onChangeInput, onInsert]
);
return (
<TodoList
input={input}
todos={todos}
onChange={onChange}
onSubmit={onSubmit}
onToggle={onToggle}
onRemove={onRemove}
/>
);
};
export default TodoListContainer;
이번 실습에서 사용하지 않은, 다른 Hooks 가 두개 더 있습니다.
useDispatch Hook 은 컴포넌트 내에서 dispatch 를 사용 할 수 있게 해줍니다.
import { useDispatch } from 'react-redux';
const dispatch = useDispatch();
useStore Hook 은 컴포넌트 내에서 store 를 사용 할 수 있게 해줍니다.
import { useStore } from 'react-redux'
const store = useStore();
원래 기존에 react-redux 에서는 connect 를 통해 컴포넌트에서 리덕스 스토어에 연결을 했지만, 이제는 Hooks 로도 할 수 있게 됐습니다. 물론, 아직 정식 릴리즈 된건 아니지만, 만약 정식 릴리즈가 됐을때 기존 connect 를 사용 하는 컨테이너 컴포넌트를 모두 수정해주어야 할까요?
그렇지는 않습니다. 어쩌다가 특정 컴포넌트를 리팩토링하게 될 때는 겸사겸사 Hooks 를 사용하는 코드로 바꿔주면 좋긴 하겠지만, connect 함수가 deprecated 되는 것이 아니기 때문에 기존에 잘 작동하고 있는 컨테이너 컴포넌트를 굳이 Hooks 로 전환해줄 필요는 없습니다.
단, 새로운 컴포넌트를 만들게 될 때는 Hooks 를 사용하는 것을 추천드립니다. 아무래도 매우 편하기도 하니까요. (어디 까지나 정식 릴리즈 됐을 때 얘기입니다. 알파일때는 프로덕션에서 사용하지 마세요. 나중에 귀찮아지는 일이 벌어질수도 있습니다.)
읽어주셔서 감사합니다 :) 오탈자 있으면 피드백 부탁드려요.
안녕하세요~ Velopert님의 글을 보면서 열심히 공부하고 있습니다 :)
hooks 공부하려고 이 글을 쭉 보면서 실습을 따라했는데,
TypeError (0 , _reactRedux.useActions) is not a function
이런 에러가 뜹니다 ㅠㅠ 그래서 제 로컬 설정 이슈인가 싶었는데
본문 중간에 [Edit on CodeSandBox] 링크 걸어놓으신 곳에서도 두 군데 다 에러가 나고 있네요~! 이 부분 어떻게 수정해야 할까요? ㅠㅠ
테마가 궁금해서요!!
Material Theme 쓰신건가욤?? 저는 lighter 쓰는데 velopert님꺼는 조금더 회색이 잘보이는 것 같아서요!!
https://react-redux.js.org/next/api/hooks#recipe-useactions
useActions 도 뭔가 조치에 들어간거 같네요 ㅠㅠ
useDispatch 를 사용하시기 바랍니다.
언제 또 수정될런지 ㅋㅋㅋ
velopert 님 잘보고 배워갑니다. 항상 감사합니다.
const result : any = useSelector(selector : Function, equalityFn? : Function)
useSelector 두 번째 파라미터도 바뀌었어요. 예제 처럼 빈 배열을 넣으면 같은 값이여도 값을 새로 불러오더라고요.
아무 값을 안 넣으면 === 으로 비교한다고 하네요.
import { shallowEqual, useSelector } from 'react-redux'
// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)
이렇게 하면 React.memo와 같이 shallow compare로 동작한다고 합니다.
확인 부탁드려요.
export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });
const dispatch = useDispatch();
const onIncrease = useCallback(() => dispatch(increment()), [dispatch]);
const onDecrease = useCallback(() => dispatch(decrement()), [dispatch]);
혹시 이렇게 하는게 맞을까요..? 아무나 좋은 예시 부탁드립니다... 😢
https://codesandbox.io/s/react-redux-hooks-tutorial-1kivx
대부분 자체 해결하셨겠지만, 삭제된 기능(useActions/useRedux)을 제외하도록 수정된 코드 공유합니다.
좋은 포스팅 항상 감사히 보고 있습니다.
https://github.com/reduxjs/react-redux/issues/1252#issuecomment-487396801
예상하셨던 것처럼 useRedux가 사라졌네요!