드디어 리덕스를 배우게 되었다!
이번주 금요일 오전엔 리덕스에 대한 개념을 정리하고, 오후엔 조그마한 커머스 페이지(장바구니에 상품 추가, 삭제 및 수량 조절 등의 기능을 가지고 있음)를 만들어보았다.
추상적인 개념들이 많기 때문에... 각 개념의 역할들을 충분히 숙지하기 위하여 이번 파트만큼은 achievement goals 내용을 아래와 같이 정리해보았다.
개발하고자 하는 프로그램의 컴포넌트 구성이 (2-3개의 레벨로 구성된 게 아니라) 굉장히 복잡해질 때, props로 상태를 전달해주는 것이 한계가 있을 것이다. props는 '리액트의 단방향 데이터 흐름 원칙에 따라' 최하위 컴포넌트에서 상위 state의 정보가 필요하다면,,,? state가 선언된 컴포넌트로부터 매 컴포넌트를 거쳐서 props가 아래 자식 컴포넌트로 전달되어져야 하기 때문이다. 코드 작성 뿐 아니라 상태 관리 자체가 굉장히 복잡하고 비효율적이다.
이러한 문제점을 해결하고자 리덕스와 같은 상태 관리 라이브러리가 등장하게 되었다.
Redux는 React의 관련 라이브러리, 혹은 하위 라이브러리가 아니다. Redux는 React 없이도 사용할 수 있는, 상태 관련 라이브러리이다.
상태에 어떠한 변화가 필요하게 될 때 액션이란 것을 발생시킵니다. 이는 하나의 객체로 표현되는데 액션 객체는 다음과 같은 형식으로 이뤄져있다. 액션 객체는 type
필드를 필수적으로 가지고 있어야한다.
{
type: "ADD_TODO",
payload: {
id: 0,
text: "리덕스 배우기"
}
}
액션 생성함수(action creator)는 액션을 만드는 함수이다. 단순히 파라미터를 받아와서 액션 객체 형태로 만들어준다.
export function addTodo(data) {
return {
type: "ADD_TODO",
payload:
//... ///
};
}
이러한 액션 생성함수를 만들어서 사용하는 이유는 나중에 컴포넌트에서 더욱 쉽게 액션을 발생시키기 위함이다. 그래서 보통 함수 앞에 export 키워드를 붙여서 다른 파일에서 불러와서 사용한다.
리덕스를 사용 할 때 액션 생성함수를 사용하는것이 필수적이진 않다. 액션을 발생 시킬 때마다 직접 액션 객체를 작성할수도 있다.
리듀서는 변화를 일으키는 함수이다. 리듀서는 두가지의 파라미터를 받아오고, 기본형태는 아래와 같다.
function reducer(state, action) {
// 상태 업데이트 로직
return alteredState;
}
리듀서는, 현재의 상태와 전달 받은 액션을 참고하여 새로운 상태를 만들어서 반환합니다. 이 리듀서는 useReducer 를 사용할때 작성하는 리듀서와 똑같은 형태를 가지고 있습니다.
리액트의 useReducer
에선 일반적으로 default:
부분에 throw new Error('Unhandled Action')
과 같이 에러를 발생시키도록 처리하는게 일반적인 반면 리덕스의 리듀서에서는 기존 state를 그대로 반환하도록 작성해야합니다.
리덕스를 사용 할 때에는 여러개의 리듀서를 만들고 combineReducers
라는 메소드로 루트 리듀서 (Root Reducer)를 만들 수 있습니다.
const rootReducer = combineReducers({
itemReducer,
notificationReducer
});
리덕스에서는 한 애플리케이션당 하나의 스토어가 존재한다. 스토어 안에는, 현재의 앱 상태와, 리듀서가 들어가있고, 추가적으로 몇가지 내장 함수들이 있다.
디스패치는 스토어의 내장메소드 중 하나이다. 디스패치는 액션을 발생 시킨다. dispatch 라는 함수에는 액션을 파라미터로 전달합니다 dispatch(action)
이렇게 호출을 하면, 스토어는 리듀서 함수를 실행시켜서 해당 액션을 처리하는 로직이 있다면 액션을 참고하여 새로운 상태를 만들어준다.
(출처: 코드스테이츠)
정리를 해보자면...
Action 객체는 Dispatch에게 전달되고, Dispatch는 Reducer를 호출해서 새로운 state를 생성한다.
useSelector
는 리덕스의 상태값을 조회하기 위한 hook 함수이다. 컴포넌트와 state를 연결하는 역할을 한다. 컴포넌트에서 useSelector 메소드를 통해 store의 state에 접근할 수 있는 것이다.
useSelector의 전달인자로는 콜백 함수를 받으며 콜백 함수의 전달인자로는 state 값이 들어간다.
useDispatch
는 Action 객체를 Reducer로 전달해주는 메소드입니다. Action 이 일어날만한 곳은 클릭 등의 이벤트가 일어나는 컴포넌트이다.
오늘 스프린트 과제로 만들어냈던 애플리케이션의 컴포넌트 구조는 아래와 같다.
중간레벨에 위치한 ShoppingCart라는 컴포넌트 내에서 아래와 같이 useSelector, useDispatch를 사용하였다.
import { useDispatch, useSelector } from 'react-redux'
//...//
export default function ShoppingCart() {
const state = useSelector(state => state.itemReducer); //itemReducer로 인해 변경되는 'state'를 조회하여
const { cartItems, items } = state //사용하기 쉽게 구조분해 할당...!
const dispatch = useDispatch();
//...//
}
dispatch 메소드에 전달인자로 action 생성함수가 전달된다. 해당 action생성함수의 전달인자가 무엇인지 확인하여 onClick이벤트가 발생할 때 전달되는 전달인자와의 상관관계를 분명히 해야 한다(ex. item & item.id)
function ItemListContainer() {
const state = useSelector(state => state.itemReducer);
const { items, cartItems } = state;
const dispatch = useDispatch();
const handleClick = (item) => {
if (!cartItems.map((el) => el.itemId).includes(item.id)) {
dispatch(addToCart(item.id)) //TODO: dispatch 함수를 호출하여 아이템 추가에 대한 액션을 전달한다
}
return (
//...//
{items.map((item, idx) => <Item item={item} key={idx} handleClick={() => {
handleClick(item) //이벤트 발생!
}} />)}
);
}
참고한 자료
https://youtu.be/CVpUuw9XSjY :스프린트를 시작하기에 앞서 간단한 리덕스 튜토리얼을 직접 따라해보았다.
https://react-redux.js.org/api/hooks : 리액트 공식문서 내 hooks 파트
더 읽어보고 싶은 자료
https://velog.io/@dolarge/React-Container-Presenter-%ED%8C%A8%ED%84%B4 리액트에서 Presentational 컴포넌트와 Container 컴포넌트의 개념