1. 작업 환경
사용한 라이브러리
- redux ( npm add react-redux )
- immer ( npm add immer )
2. Presentational Component
프레젠테이셔널 컴포넌트 : 주로 props를 받아와 사용자에게 받아온 정보를 보여주는 컴포넌트
1) 카운터 Counter.js
const Counter = ({ number, onIncrease, onDecrease }) => {
return (
<div>
<h1>{number}</h1>
<div>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
</div>
);
};
export default Counter;
2) Todos.js
const TodoItem = ({ todo, onToggle, onRemove }) => {
return (
<div>
<input
type="checkbox"
onClick={() => onToggle(todo.id)}
checked={todo.done}
readOnly={true}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={()=>onRemove(todo.id)}>삭제</button>
</div>
);
};
const Todos = ({
input, // 인풋에 입력되는 텍스트
todos, // 할 일 목록이 들어있는 객체
onChangeInput,
onInsert,
onToggle,
onRemove
}) => {
const onSubmit = e => {
e.preventDefault();
onInsert(input);
onChangeInput('');
};
const onChange = e => onChangeInput(e.target.value);
return (
<div>
<form onSubmit={onSubmit}>
<input value={input} onChange={onChange} />
<button type="submit">등록</button>
</form>
<div>
{todos.map(todo => (
<TodoItem
todo={todo}
key={todo.id}
onToggle={onToggle}
onRemove={onRemove}
/>
))}
</div>
</div>
);
};
export default Todos;
카운터 컴포넌트는 어렵지 않게 작성했지만, Todos는 조금 복잡해보인다.
우선 Todos는 정보를 입력하는 컴포넌트이고 입력한 정보는 TodosItem컴포넌트의 양식에 맞게 리스트가 생산된다.
3. modules
기본적으로 redux를 사용하여 action, constants, reducers 3가지 디렉터리로 나누어 기능별로 파일을 만드는 방식이 있지만, 이번에는 한 파일에 작성하는 Ducks패턴을 이용해 보자
우선 1️⃣액션 타입 정의를 하고, 2️⃣ 액션 생성 함수를 만들고 , 3️⃣ 리듀서를 작성 의 순서로 작성해 주자
어느 한개가 빠지면 동작하지 않는다.
1) counter 모듈
import { createAction, handleActions } from 'redux-actions';
// 액션 타입 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
//액션 생성 함수 만들기
// export const increase = () => ({ type: INCREASE });
// export const decrease = () => ({ type: DECREASE });
//redux-action을 이용한 액션 생성 함수 생성
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);
//초기 상태 및 리듀서 함수 만들기
const initialState = {
number: 0
};
// const counter = (state = initialState, action) => {
// switch (action.type) {
// case INCREASE:
// return {
// number: state.number + 1
// };
// case DECREASE:
// return {
// number: state.number - 1
// };
// default:
// return state;
// }
// }
//handleActions를 이용한 방법
const counter = handleActions(
{
[INCREASE]: (state, action) => ({ number: state.number + 1 }),
[DECREASE]: (state, action) => ({ number: state.number - 1 }),
},
initialState,
);
export default counter;
위의 코드에서 주석 처리된 코드는 Hooks를 사용하지 않고 작성한 코드이며
개인적으로는 Hooks를 사용한 코드가 더 가독성이 좋고 작성하기 쉬운거 같다.
handleActions 함수의 첫번째 파라미터는 각 액션에 대한 업데이트 함수이고,
두번째 파라미터는 초기 상태를 넣어준다.
2) todos 모듈
import { createAction, handleActions } from 'redux-actions';
import produce from 'immer';
// 액션 타입 정의
const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 인풋 값을 변경
const INSERT = 'todos/INSERT'; // 새로운 todo값을 등록
const TOGGLE = 'todos/TOGGLE'; // todo를 체크/체크 해제
const REMOVE = 'todos/REMOVE'; // todo를 제거
// 액션 생성 함수 만들기
// export const changeInput = input => ({
// type: CHANGE_INPUT,
// input
// });
// let id = 3; // insert가 호출될 때마다 1씩 더해짐
// export const insert = text => ({
// type: INSERT,
// todo: {
// id: id++,
// text,
// done: false
// }
// });
// export const toggle = id => ({
// type: TOGGLE,
// id
// });
// export const remove = id => ({
// type: REMOVE,
// id
// });
//createActions를 이용한 액션 생성 함수 만들기
export const changeInput = createAction(CHANGE_INPUT, input => input);
let i = 3;
export const insert = createAction(INSERT, text => ({
id: i++,
text,
done: false,
}));
export const toggle = createAction(TOGGLE, id => id);
export const remove = createAction(REMOVE, id => id);
// 초기 상태 및 리듀서 함수 만들기 (불변성 유지)
const initialState = {
input: '',
todos: [
{
id: 1,
text: '리덕스 기초 배우기',
done: false
},
{
id: 2,
text: '리액트와 리덕스 사용하기',
done: false
}
]
};
//handleActions로 작성한 리듀서
const todos = handleActions(
{ //기존 방식에 immer 적용
[CHANGE_INPUT]: (state, action) =>
produce(state, draft => {
draft.input = action.payload;
})
,
//객체 비구조화 할당 문법으로 코드에 immer적용
[INSERT]: (state, { payload: todo }) =>
produce(state, draft => {
draft.todos.push(todo);
})
,
[TOGGLE]: (state, { payload: id }) =>
produce(state, draft => {
const todo = draft.todos.find(todo => todo.it === id);
todo.done = !todo.done;
}),
[REMOVE]: (state, { payload: id }) =>
produce(state, draft => {
const index = draft.todos.findIndex(todo => todo.id === id);
draft.todos.splice(index, 1);
}),
},
initialState,
)
// const todos = (state = initialState, action) => {
// switch (action.type) {
// case CHANGE_INPUT:
// return {
// ...state,
// input: action.input
// };
// case INSERT:
// return {
// ...state,
// todos: state.todos.concat(action.todo)
// };
// case TOGGLE:
// return {
// ...state,
// todos: state.todos.map(todo =>
// todo.id === action.id ? { ...todo, done: !todo.done } : todo
// )
// };
// case REMOVE:
// return {
// ...state,
// todos: state.todos.filter(todo => todo.id !== action.id)
// };
// default:
// return state;
// }
// }
export default todos;
앤션 생성 함수에서 받아온 파라미터를 payload에 넣는 것이 아니라 변형을 주어 넣고싶다면,
createActions의 두 번째 함수에 payload를 정의하는 함수를 따로 선언하여 넣어주면 된다.
insert는 todo객체를 액션객체에 얺어줘야 하므로 text를 넣으면 todo객체가 반환하도록 하였다.
또한 액션 생성 함수는 액션에 필요한 추가 데이터를 payload 라는 이름을 사용하기에 action.payload값을
조회해야 하지만, 위의 코드에서는 객체 비구조화 할당으로 action값의 payload 이름을 새로 설정했다.
그리고 불변성을 지키기 위해 immer라이브러리를 사용했다.
기존 방식과 객체 비구조화 할당 문법을 이용해 작성한 두 방식의 코드를 immer이용해 작성했다.
4. Container Component
컨테이너 컴포넌트 : 리덕스와 연동되있으며, 리덕스로 부터 상태를 받아 오기도 하고 리덕스 스토어에
액션을 디스패치하기도 하는 컴포넌트
컨테이너는 Hooks를 사용하지 않은 것과 사용한 것을 만들어 봤다.
-Hooks 미사용
1) CounterContainer
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Counter from '../component/Counter';
import { increase, decrease } from '../modules/counter';
const CounterContainer = ({ number, increase, decrease }) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease} />
);
};
const mapStateToProps = state => ({
number: state.counter.number,
});
const mapDispatchToProps = dispatch => ({
//임시함수
increase: () => {
dispatch(increase());
},
decrease: () => {
dispatch(decrease());
},
});
//bindActionCreators 유틸 함수를 이용한 액션함수 dispatch로 감싸기
//connect 함수 내부에 익명 함수 형태로 넣어도 된다.
// const mapDispatchToProps = dispatch =>
// bindActionCreators(
// {
// increase,
// decrease,
// },
// dispatch,
// )
export default connect(mapStateToProps, mapDispatchToProps,)(CounterContainer);
//mapDispatchToProps의 파라미터를 액션 생성함수로 이루어진 객체 형태로 넣어 사용하는 방법
// export default connect(
// state => ({
// number: state.counter.number
// }),
// { //이렇게 해주면 connect함수가 내부적으로 bindActionCreators 작업을 해준다.
// increase,
// decrease
// }
// )(CounterContainer);
//아래와 같이 connect 함수 내부에 익명 함수 형태로 선언해도 문제가 되지 않는다.
// export default connect(
// state => ({
// number: state.counter.number
// }),
// dispatch => ({
// increase: () => {
// dispatch(increase());
// },
// decrease: () => {
// dispatch(decrease());
// },
// })
// )(CounterContainer);
connect 함수를 사용할 때 일반적으로 mapStateToProps와 mapDispatchToProps를 미리 선언해 놓고 사용한다.
하지만 connect함수 내부에 익명 함수 형태로 선언해도 문제가 되지 않는다.
액션 생성 함수가 많아지면, 액션을 디스패치하기 위해 액션 생성 함수를 호출하여
dispatch로 묶는 과정이 복잡해진다.
이를 bindActionCreators 유틸 함수를 사용하면 간단해 진다.
bindActionCreators보다 간단한 방법이 있다.
mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성 함수로 이루어진 객체 형태로 넣어주면 된다
2) TodosContainer
import { connect } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/todos';
import Todos from '../component/Todos';
const TodosContainer = ({
input,
todos,
changeInput,
insert,
toggle,
remove
}) => {
return (
<Todos
input={input}
todos={todos}
onChangeInput={changeInput}
onInsert={insert}
onToggle={toggle}
onRemove={remove}
/>
);
};
export default connect(
//비구조화 할당을 통해 todos 분리
//state.todos.input 대신 todos.input 사용
({ todos }) => ({
input: todos.input,
todos: todos.todos,
}),
{
changeInput,
insert,
toggle,
remove,
},
)(TodosContainer);
-Hooks 사용
useSelector와 useDispatch를 사용해 작성한다.
useSelector
- connect 함수를 사용하지 않고 리덕스의 상태를 조회할 수 있다.
- const 결과 = useSelector( 상태 선택 함수 ); 의 방식으로 사용한다.
- 상태 선택 함수는 mapStateToProps와 형태가 같다.
useDispatch
- 컴포넌트 내부에서 dispatch를 사용할 수 있게 해주는 Hook이다.
- 컨테이너 컴포넌트에서 디스패치해야 한다면 이 Hook을 사용하자
- const dispatch = useDispatch();
dispatch( { type: 'SAMPLE_ACTION' } );
위와 같이 사용해 주면 된다. - 컴포넌트 성능 최적화가 필요하면, useCallback으로 액션을 디스패치하는 함수를 감싸주자(중요)
1) CounterContainer
import { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Counter from '../component/Counter'
import { increase, decrease } from '../modules/counter';
const CounterContainer = () => {
// useSelector로 상태 조회
const number = useSelector(state => state.counter.number);
const dispatch = useDispatch();
//useCallback을 이용한 컴포넌트 최적화
const onIncrease = useCallback(() => dispatch(increase), [dispatch]);
const onDecrease = useCallback(() => dispatch(decrease), [dispatch]);
return (
<Counter
number={number}
// onIncrease={() => dispatch(increase)}
// onDecrease={() => dispatch(decrease)}
onIncrease={onIncrease}
onDecrease={onDecrease}
/>
);
};
export default CounterContainer;
2) TodosContainer
import { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux'; //connect대신 useSelector와 useDispatch를 사용
import { changeInput, insert, toggle, remove } from '../modules/todos';
import Todos from '../component/Todos';
const TodosContainer = () => {
//비구조화 할당 문법을 사용
const { input, todos } = useSelector(({ todos }) => ({
input: todos.input,
todos: todos.todos,
}));
const dispatch = useDispatch();
const onChangeInput = useCallback(input => dispatch(changeInput(input)),
[dispatch]
);
const onInsert = useCallback(text => dispatch(insert(text)), [dispatch]);
const onToggle = useCallback(id => dispatch(toggle(id)), [dispatch]);
const onRemove = useCallback(id => dispatch(remove(id)), [dispatch]);
return (
<Todos
input={input}
todos={todos}
onChangeInput={onChangeInput}
onInsert={onInsert}
onToggle={onToggle}
onRemove={onRemove}
/>
);
};
export default TodosContainer;
- useActions를 이용한 방법
import React from 'react';
import { useSelector } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/todos';
import Todos from '../components/Todos';
import useActions from '../lib/useActions';
const TodosContainer = () => {
const { input, todos } = useSelector(({ todos }) => ({
input: todos.input,
todos: todos.todos
}));
const [onChangeInput, onInsert, onToggle, onRemove] = useActions(
[changeInput, insert, toggle, remove],
[]
);
return (
<Todos
input={input}
todos={todos}
onChangeInput={onChangeInput}
onInsert={onInsert}
onToggle={onToggle}
onRemove={onRemove}
/>
);
};
export default React.memo(TodosContainer);
useActions의 첫 번째 파라미터는 액션 생성 함수로 이루어진 배열이고, 두 번째 파라미터는 deps 배열이다.
이 배열에 들어있는 원소가 바뀌면 액션을 디스패치하는 함수를 새로 만들게 된다.
- 종합
connect를 사용해 컴포넌트를 구성하면, 해당 컴포넌트의 부모의 컴포넌트가 리렌더링될 때 해당 컨테이너
컴포넌트의 props가 바뀌지 않으면 리렌더리이 자동으로 방지 되지만,
Hooks를 사용하면 최적화 작업이 자동으로 이루어지지 않으므로, React.memo를 컴포넌트에 사용해 줘야한다.
'React' 카테고리의 다른 글
React) 18장 리덕스 미들웨어를 이용한 비동기 작업관리 ( Thunk ) (0) | 2022.05.20 |
---|---|
React) 18장 리덕스 미들웨어를 이용한 비동기 작업관리( 미들웨어 ) (0) | 2022.05.20 |
React) 16장 redux 라이브러리 (0) | 2022.05.12 |
React) 15장 Context API (0) | 2022.05.11 |
React) 14장 외부 API를 이용한 뉴스 뷰어 (0) | 2022.05.10 |