드디어 리덕스를 배우게 되었다!
이번주 금요일 오전엔 리덕스에 대한 개념을 정리하고, 오후엔 조그마한 커머스 페이지(장바구니에 상품 추가, 삭제 및 수량 조절 등의 기능을 가지고 있음)를 만들어보았다.
추상적인 개념들이 많기 때문에... 각 개념의 역할들을 충분히 숙지하기 위하여 이번 파트만큼은 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 컴포넌트의 개념