Redux
자바스크립트 애플리케이션에서 클라이언트 상태를 효율적으로 관리할 수 있도록 해주는 도구입니다.
복잡한 상태관리가 일어나는 SPA 어플리케이션에서 자주 사용됩니다.
이 글에서는 React
에서 Redux
를 사용해야 하는 이유에 대해서 다룹니다.
리액트에서는 상태가 변화하면 이를 감지하여 상태를 화면에 반영하기 위해 상태를 공유하는 컴포넌트에서 리렌더링이 발생하게 됩니다.
즉, 하나의 상태가 변화하게 됐을 때, 해당 상태를 공유하고 있는 모든 컴포넌트들이 리렌더링 됩니다. 이는 애플리케이션 자체의 성능을 저하시킬 수 있습니다.
그렇기 때문에 애플리케이션의 상태를 관리함에 있어서, 상태를 효율적으로 관리하기 위한 노력이 필요합니다.
useState
+ Context API
기본적인 리액트의 상태 관리는 useState
훅을 통해 이루어집니다.
상태를 필요로 하는 컴포넌트에 상태와 상태를 업데이트하는 함수를 props로 전달함으로써, 해당 컴포넌트가 상태 값을 사용하고 변경할 수 있게 됩니다."
아주 작은 간단한 소규모의 프로젝트에서는 prop drill을 통한 상태 공유 방식이 적절할 수 있습니다.
프로젝트의 규모가 조금씩 커져, 여러 컴포넌트가 하나의 값이 필요로 한다면?
값을 필요로 하지 않는 컴포넌트를 거쳐 데이터가 전달될 수 있고, 불필요한 props drilling
이 발생할 수 있습니다.
이때 불필요한 props drilling
을 피하기 위해서 사용할 수 있는 기능이 바로 리액트의 Context API
입니다.
Context API
를 사용하여 데이터를 필요로하는 컴포넌트를 Provider
로 감싸서 데이터를 하위 컴포넌트에게 전달하게 되면 props drilling
을 피하고 상태를 전역적으로 관리할 수 있습니다.
🤔 그럼
useState
와Context API
만 사용해 상태를 관리하면 되잖아요
상태가 컨트롤 가능한 정도 갯수의 작은 프로젝트에서는 이렇게 관리하는 것이 좋을 수 있습니다.
하지만 복잡한 형식의 데이터를 다루는 데는 한계가 있으며, 상태의 변화를 예측하기 어렵습니다.
useReducer
+ Context API
useReducer
는 복잡한 상태를 관리하기 위해 사용되는 리액트의 내장 훅입니다.
간단히 설명하자면 초기 상태와, 상태를 업데이트하는 reducer
함수를 인자로 받는 훅입니다.
반환값은 state
와 dispatch
순의 배열으로 상태와 상태를 업데이트 하는 함수를 반환합니다.
상태를 업데이트하기 위해서 type
과 payload
을 가지는 action
객체를 dispatch
의 인자로 전달해주어야 합니다.
useReducer
를 사용해 데이터를 업데이트하는 간단한 예시입니다.
// 상태를 업데이트하는 리듀서 함수
const reducer = (state, action) => {
// action의 type에 따라 상태 변경
switch(action.type) {
case 'SET_NAME':
if (!/^[가-힣]{2,8}$/g.test(action.payload)) {
throw new Error('이름이 올바른 형식이 아닙니다.');
}
return { ...state, username: action.payload };
default:
return state;
}
};
// 기본 상태를 가지는 객체
const initialState = {
username: ''
};
// 상태, 상태를 업데이트하는 dispatch 함수를 반환
const [state, dispatch] = useReducer(reducer, initialState)
const setName = (name) => {
// type과 payload를 가지는 action 객체를 dispatch의 인자로 전달
dispatch({
type: 'SET_NAME',
payload: name,
})
}
reducer
함수를 통해 데이터를 업데이트하는 방식을 미리 작성하고 사용하기 때문에, useState
의 문제점인 의도치 않은 동작을 미리 방지할 수 있습니다.
또한 비즈니스 로직을 reducer
함수가 가지고 있기 때문에 유지보수에 유리합니다.
useReducer
+ Context API
를 조합해 사용하게 되면 redux
의 기능을 대부분 구현할 수 있습니다.
- 상태 변경에 대해서 리덕스는 동기적으로 처리하고
useReducer
는 비동기적으로 처리합니다.
- 리덕스가 제공하는 강력한 미들웨어 기능이 있습니다.
미들웨어에 대해서 간단하게 설명드리자면, 상태를 업데이트 하는 요청이 들어왔을 때 상태를 업데이트하기 이전에 어떠한 작업을 할 수 있는 기능을 제공합니다.
대표적으로 로깅을 위한 redux-logger
, 비동기 작업을 위한 redux-thunk
, redux-saga
등이 있습니다.
Context
와useReducer
를 조합해 사용하게 되면 상태가 업데이트 됐을 때 상태를 사용하는 모든 컴포넌트가 렌더링 되는 문제가 있습니다.
dispatch
는 기존의 상태 객체를 전개 구문을 통해 복사하고, action
객체의 payload
로 전달되는 값을 통해 특정 프로퍼티를 덮어씌워 새로운 객체를 반환하는 방식입니다.
때문에 하나의 Context
로 상태를 전달받아 사용하는 구조라면 변경된 데이터를 사용하지 않는 컴포넌트도 렌더링 되는 문제가 발생할 수 있습니다.
물론 이런 문제를 해결하기 위해 Context
를 잘게 쪼갤 수도 있겠지만, 비효율적이고 복잡한 작업이 될 것입니다.
또 이를 개선하기 위해 컴포넌트를 React.Memo
를 적절히 사용하여 Redux
와 비슷한 최적화를 얻을 수도 있습니다.
하지만
Redux
는 알아서 해줍니다.
Redux
를 사용하면 React.Memo
로 컴포넌트 렌더링을 최적화하거나, Context
를 잘게 쪼개 전달할 필요가 없습니다.
리덕스를 사용하는 것만으로도 불필요한 렌더링을 줄이면서 전역으로 상태를 관리 할 수 있습니다.
Redux
는 상태 관리를 위한 라이브러리입니다.
리액트의 내장 기능으로 전역 상태 관리를 할 수 있지만, 성능 최적화를 위해 여러 가지 작업을 해야 할 수 있습니다.
하지만 리덕스를 사용하면 전역 상태 관리를 함과 더불어 성능 최적화를 위해 여러 작업을 하지 않고도 렌더링을 최적화할 수 있습니다.
마치며 리덕스를 효과적으로 사용하기 위해서 리덕스는 다음과 같은 3가지 사항을 제시합니다.
하나의 애플리케이션에서 하나의 Store만 존재한다.
state를 직접 변경해서는 안 된다. state의 변경은 Reducer만 할 수 있다.
state를 변화시키는 방법은 Action을 dispatch하는 방법뿐이다.
순수 함수: 외부 상태를 변경하지 않으며, 같은 인자에 대해 항상 같은 결과를 반환하는 함수. 즉, 부수 효과(side effect)가 없는 함수