본문 바로가기
공부/리액트

[react-redux] 리액트에서 리덕스 사용하기

by 야옹아옹 2022. 12. 28.

 

✨ 리덕스란?

자바스크립트 어플리케이션을 위한 예측 가능상태 관리 라이브러리

리덕스가 생겨난 이유

자바스크립트 싱글 페이지 어플리케이션이 갖추어야하는 요건이 복잡해지면서, 어느때보다 많은 상태를 자바스크립트 코드로 관리할 필요성이 생겨났다.

  • 여기서 상태란, 서버 응답, 캐시 데이터, 지역적으로 생성해 쓰지만 아직 서버에 저장되지 않은 데이터
  • 이외에도 활성화된 라우트, 선택된 탭, 로딩 여부, 페이지네이션 컨트롤 등의 다양한 UI 상태를 의미한다.

리덕스는 이런 복잡한 상태들을 더 편리하게 관리하기 위해서 만들어진 라이브러리이다.

리액트에서만 사용할 수 있는 것이 아니라 자바스크립트 어플리케이션에서는 리덕스를 사용할 수 있다.

리덕스의 3가지 원칙

  • 단일 출처 원칙
  • 상태는 읽기 전용
  • 리듀서는 순수 함수로 작성
단일 출처 원칙
- 어플리케이션의 모든 상태는 하나의 저장소 안에 하나의 객체 트리 구조로 저장된다.

하나의 상태 트리만 가지고 있기 때문에 복잡한 기능들을 구현하기 더 쉽다.

하나의 상태 트리만 가지고 있기 때문에 디버깅이 쉽다.

상태는 읽기 전용이다.
- 상태를 변화시키는 유일한 방법은 액션 객체를 전달하는 방법 뿐이다.

상태를 변경하기 위해서는 반드시 액션 객체를 사용해야하기 때문에, 뷰나 네트워크 콜백에서 결코 상태를 직접 바꾸지 못한다는 것을 보장한다. => 에러가 생길 가능성을 줄임

액션 객체평범한 자바스크립트 객체이다. 따라서 테스트나 디버깅을 위해서 재현하기 쉽다. 액션 객체를 만들어 전달만 하면 되기 때문이다.

리듀서는 순수 함수로 작성되야한다.
- 액션에 의해 상태 트리가 어떻게 변화하는 지를 지정하기 위해서 리듀서는 순수 함수로 작성된다.

액션이 발생하면 이전 상태를 변경하는 대신, 새로운 상태 객체를 생성해서 반환해줘야한다.

리덕스는 내부적으로 데이터가 변경되는 것을 감지하기 위해 얕은 복사를 진행한다. 따라서 상태가 변경이 되었다면 새로운 상태 객체를 넣어줘야 변경된 것을 인지할 수 있다.

let obj = { name: 'ayo'};
let obj2 = obj;
obj2.name = 'js';
// 얕은 복사의 경우
obj == obj2 // 같다.
// 이는 obj2가 obj의 메모리 주소를 참조하고 있기 때문이다.
// 그렇기 때문에 obj2는 name을 변경했어도 obj와 같고, 이름 변경은 obj도 되어있다.
// 이렇게 참조 주소가 같기때문에 내부 내용이 아무리 바뀌어도 
// 이전 obj와 같다. 
// 이런 참조 주소를 변경하기 위해서
// 아예 새로운 { } 객체로 만들어 줘야한다.
// 새로운 객체를 만들게 되면 그 새로운 객체는 다른 참조 주소를 가지게된다.
obj = {name:'gg'}; // 새로 객체를 할당하면 참조 주소가 다르다
obj == obj2 // 다르다. 
{} == {} // false다. 둘은 다르다.

 

✨ 리액트에서 리덕스 사용하기

사용하는 라이브러리

  • redux
  • react-redux

리덕스 디자인 패턴

  • 일반적인 패턴
    • actions, constants, reducers 폴더에 각각 액션, 액션 생성함수, 리듀서 파일을 따로 관리하는 방법
  • Ducks 패턴
    • 액션, 액션 생성 함수, 리듀서를 하나의 파일에서 관리하는 패턴
    • 사용할 상태에 따라 파일을 분리한다.

리덕스 코드 작성하기

Ducks 패턴을 사용해 만드는 예제이다. 파일은 redux/counter.js이다.

코드 샌드박스

https://codesandbox.io/s/redux-practice-j1ue33?file=/src/index.js 

 

redux-practice - CodeSandbox

redux-practice by 12Ahn22 using react, react-dom, react-redux, react-scripts, redux

codesandbox.io

액션 타입을 만들기
- 액션 타입은 중복이 되면 안된다. 따라서 모듈명/액션이름 형식으로 만든다.
// 액션 타입 선언
// '모듈 이름/액션 이름'
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
액션 생성함수 만들기
- 액션 생성 함수는 dispatch를 할 때 사용된다. dispatch는 컴포넌트에서 사용하기 때문에 액션 생성 함수는 외부에서 사용할 수 있어야한다.
// 액션 생성 함수 만들기
// 액션 생성 함수는 dispatch를 할 때, 사용한다.
// dispatch는 해당 컴포넌트에서 사용한다.
// 따라서 액션 생성 함수는 export를 해
// 외부에서 사용할 수 있도록 해야한다
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: 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;
  }
};

// 리듀서 함수 내보내기!
export default counter; // 내보낸 이름이 state에 사용된다. state.counter
루트 리듀서 만들기
- 여러개의 리듀서를 두고 싶은 경우, 여러개의 리덕스 파일이 만들어진다. 이 리듀서들을 하나로 합쳐주어야한다.
- 단일 책임 원칙에 의해서 리덕스는 하나의 스토어를 가져야하기 때문이다.

예제 코드에서는 counter 상태를 하나 사용하기 때문에 루트 리듀서를 만들지 않고 counter 리듀서를 사용해 스토어Store를 만들어 사용해도 되지만 실제로 하나를 사용하는 경우는 거의 없고 그런 상황이라면 리덕스를 쓰지않는게 좋다.

 

redux 폴더에 index.js파일을 생성 후, 해당 파일에 루트 리듀서를 생성해준다. 루트 리듀서는 스토어를 만드는데 사용된다.

// redux/index.js
// 리듀서 함수를 합치는 루트 리듀서
import { combineReducers } from 'redux';
import counter from './counter';

const rootReducer = combineReducers({
  counter,
});

export default rootReducer;
스토어를 만들어서 <App>에 연결해주기
- 만든 루트 리듀서로 스토어를 만들고 어플리케이션과 연결해줘야한다.

리덕스는 하나의 저장소에서 상태를 가져와 사용한다.(단일 출처) 따라서 저장소가 있어야하고, 해당 저장소는 어플리케이션 최상위에 존재해야한다.

// React 18v
import { createRoot } from "react-dom/client";
import { createStore } from "redux";
import { Provider } from "react-redux";

import App from "./App";
import { rootReducer } from "./redux";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

// store
const store = createStore(rootReducer);

root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

이렇게 생성한 store와 어플리케이션까지 연결이 되었다면, 컴포넌트에서 리덕스 스토어에 있는 상태를 사용할 수 있다.

 

리덕스 상태값 사용하기

connect 유틸 함수를 사용하는 방법도 있지만, Hook을 사용하는 방법이 쉽기때문에 Hook을 사용하는 방법을 사용하겠다.

Hooks들은 react-redux에 존재한다. redux가 아님을 주의하자.
useSelector(상태 선택 함수)
- 저장소에서 상태를 선택해 가져올 수 있다.
// counter 리듀서에서 만든 number 상태값을 가져온다.
const number = useSelector((state)=> state.counter.number);
useDispatch()
- 액션을 전달 받아 해당 액션이 발생하도록 전달 한다. 
const dispatch = useDispatch();

const increaseHandler = ()=>{
	// increase()는 액션 생성 함수다. {type:'counter/INCREASE'} 객체를 반환한다.
	// 즉, 액션인 {type:'counter/INCREASE'}가 디스패치에 전달되고 해당 액션이 발생한다.
	dispatch(dispatch(increase()));
}
useStore()
- 컴포넌트 내부에서 리덕스 스토어 객체를 직접 사용할 수 있다.

정말 어쩔 수 없는 경우를 제외하고는 사용하지않는 게 좋다.

 

✨정리

  • 리덕스는 복잡해지는 어플리케이션의 상태를 좀 더 쉽게 관리하기 위해 나온 라이브러리이다.
  • 리덕스는 리액트에서만 사용할 수 있는 게 아니라, 자바스크립트 어플리케이션이면 사용할 수 있는 상태 관리 라이브러리다.
  • 리액트에서 리덕스를 사용하는 경우 react-redux를 사용하면 Hook을 사용해 편리하게 사용 가능하다.

 


✨ 사담

회사에 다닐 때, 리덕스 - 리덕스 사가를 사용했었다. 이때 느낀 점은 정말 정말 정말 쓸 코드가 많다는 것이다. 특정 액션 하나만 추가해도 만들어야하는 코드가 너무 많았다. 심지어 반복 노가다 작업이니 이걸 어떻게 줄일지에 대해 많이 고민했던 게 기억난다.

벌써 1년이 넘은 경험이라 그런지 요새는 리덕스 대신 리코일, 리액트 쿼리, jotai 등등 다른 라이브러리를 사용하는 것 같다. 나도 이번에는 리덕스 대신 다른 라이브러리를 사용해 프로젝트를 진행할 예정이다.

 

댓글