[React 완벽 가이드] Section 19: Redux

gonn-i·2024년 7월 1일
0

React 완벽 가이드

목록 보기
14/18
post-thumbnail

본 포스트는 Udemy 리액트 완벽가이드 2024 를 듣고 정리한 내용입니다.

목차 🌳
1️⃣ Redux 가 뭔가요? 👀
2️⃣ Redux 그거 어떻게 쓰는건데요? 🥸
3️⃣ 적용해보자!


Redux

Redux가 뭔데요 👀

“app-level state” management!
Redux여러 컴포넌트, 혹은 앱 전체에서 공유되는 상태를 관리하기 위한 라이브러리

어랏 전역 상태관리를 위해, Context API를 전까지 썼는데, 왜 우린 redux를 또 배워야 할까요?

Context API vs Redux

context API 에는 잠재적인 단점이 존재함

context API 뭐가 문제가 될 수 있을까? 😑
1️⃣ 복잡한 설정과 관리의 위험성 : 대규모 애플리케이션에서 사용될 경우, 아래와 같은 코드가 나올 수 있음

  • ContextProvider 가 너무 많아지면, 중첩에 중첩이 더해진 jsx 코드가 나올 수 있음
  • 하나의 Context 안에 다 넣어도, 그것도 문제. 내부가 너무 비대해지기 떄문

    2️⃣ 잠재적 성능적 문제 : 데이터가 자주 변경되는 경우에, 성능 저하를 일으킬 수 있다고도 함 (w. 2018 react 직원.. )

소형 및 중형 애플리케이션에서 (어쩌면 대형에서도) context api 사용이 크게 문제 되지 않을 수 있으나, 한계가 있을 수 있음을 알고 그에 대한 대안으로 리덕스를 알아보자!


Redux 그거 어떻게 쓰는건데요? 🥸

🔥 Redux 를 쓰는 3가지 원칙
1️⃣ 하나의 저장소로부터 비롯된다
애플리케이션의 모든 상태하나의 저장소 안하나의 객체 트리 구조로 저장
2️⃣ 상태는 오직 읽기만 가능!
상태를 변화시키는 유일한 방법은 무슨 일이 벌어지는지를 묘사하는 액션 객체를 전달하는 방법뿐!
3️⃣ 상태 변화는 리듀서 함수를 통해 작성된다
액션에 의해 상태 트리가 어떻게 변화하는지를 지정하기 위해 리듀서를 통해 작성해야함!
리듀서 == 이전 상태와 액션을 받아 다음 상태를 반환하는 함수

대략적인 매커니즘🔄

Component -- dispatch -- Reducer Function -- change state -- Central Data(State) Store -- subscribe -- Component

Component 에서 dispatch 함수를 통해, action 에 따라 Reducer Function 이 실행되면서 인제 중앙 저장소에 있는 state가 변경되는데. 또 인제 이걸 구독하는 Component 가 변경된 state로 인해 재랜더링된다


createStore()

원래 Redux toolkit 사용이 권장되나, 일단 createStore() 로 먼저 리덕스의 작동 방식과 기능을 살펴본다
(뒤에서 툴킷 다룰 예정)

createStore로 사용해보자!

1️⃣ 📁 store 폴더 안에, storereducer function 만들어주기

단, 이때 리듀서 함수는 순수 함수로서, 동일한 input에 대해소 항상 동일한 output을 반환해야 한다. 따라서, 함수 내부에서는 side Effect을 다룰 수 없는데, http 요청이나, 브라우저단에서 임시 저장(local storage, session storage .. ), 타이머 설정과 같은 것이 이에 포함된다.

또한, 리듀서는 항상 새로운 상태 객체를 반환해야 해야 하기 때문에 (새로운 state는 업데이트 사항을 overwrite 해서 반환된것!!), 🙅🏻‍♀️함수 내부에서 직접 상태를 변경하면 안된다 🙅🏻‍♀️
원본 state를 절대절대절대절대 변형하지 말것 !!! (직접 변경하는 경우: state.count += 1; )
왜냐면,
1️⃣ 리듀서 외부에서 상태 변경을 추적하기 어려워짐 +
2️⃣ 참조 투명성을 잃어, 이전 상태와의 관계가 불분명해지기 때문이다. 이로 인해 예측 불가능한 동작이 발생할 수 있다.

따라서 다음과 같이 완전히 새로운 상태에 대한 객체를 반환해야 한다.
count: state.count + 1
(새로운 객체를 복사하고 생성해서 다룰 것)

상태를 여러개 다룰 경우, but 한가지 상태만을 바꾸고 싶을 경우
요렇게 spread 연산자 써서 유지할건 유지하고, 바꿀건 원래 다음과 같이 덮어써서
새로운 객체를 반환해야한다!⭐️

    return {
      ...state,
      counter: state.counter - 1,
    };

store -> index.js

import { createStore } from 'redux';

// 상태 업데이트를 담당할 리듀서 함수 (이때 내부에는 side Effect을 다룰 수 없음)
// 리듀서 함수는 새로운 상태에 대한 new 스냅샷을 생성 
// 1) 리듀서 함수의 인자로, 상태값과 액션을 담아서 작성
// 이때 state에는 초기 실행의 경우를 고려하여, default 값을 넣어줘야함 + action은 주문서라고 생각하기 -> 어떻게 바꿔주기 주문서
function counterReducer(state = { counter: 0 }, action) {
  if (action.type === 'increment') {
    return {
      counter: state.counter + 1,
    };
  }
  if (action.type === 'decrement') {
    return {
      counter: state.counter - 1,
    };
  }
  return state;
}

// 상태를 저장할 저장소를 만드는 방법
// redux 에서 createStore 함수를 실행하고, 인자로 자장소를 변경시키는 리듀서 함수를 포인터로 넘겨줄 것
const store = createStore(counterReducer);

export default store;

2️⃣ store 넘겨주기

컴포넌트 트리의 최상단에서, Provider루트 component 감싸주기
꼭 최상위 컴포넌트를 감쌀 필요는 없지만, 이렇게 되면 하위 컴포넌트에서 저장소에 도달할 수 있게 된다~
<Provider> 로 감싸주면서, store props에 직접 만든 리덕스 저장소 꼭 전달해주기!

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './store';

import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  // store에서 제공하는 state를 전달해주고 싶은 컴포넌트에 감싸준다. (부모에 감싸면 하위 컴포넌트까지 저장소에서 데이터 받을 수 있겠죠)
  // 이때 store props 에 설정한 리덕스 저장소를 전달해줌
  <Provider store={store}>
    <App />
  </Provider>
);

3️⃣ store & reducer function 실제 사용해보기

🔍 Redux 스토어의 상태를 읽는 방법
useSelector : 특정 컴포넌트에서 특정 상태 조각(state slice)만 구독(subscribe) 하고, 해당 상태가 변경될 때만 리렌더링되게 한다.
-> 자동으로 업데이트 되며, 최신 상태를 가져옴
const counter = useSelector((state) => state.counter);

🔍 action을 dispatch하여 스토어 안의 상태를 바꿔보자!
useDispatch : Redux storeaction디스패치(dispatch)하여, store의 상태를 변경해서 상태 변경을 트리거한다!

import classes from './Counter.module.css';
import { useSelector, useDispatch } from 'react-redux';

const Counter = () => {
  // Redux store 구독 및 상태 조각 가져오기
  const counter = useSelector((state) => state.counter);

  const toggleCounterHandler = () => {};

  // useDispatch 훅을 통해, dispatch 함수를 반환하고
  // dispatch 함수에 action 을 전달하여, store 내부의 상태 변경을 트리거
  const dispatch = useDispatch();
  const IncrementHandler = () => {
    dispatch({ type: 'increment' });
  };

  const DecrementHandler = () => {
    dispatch({ type: 'decrement' });
  };

  return (
    <main className={classes.counter}>
      <h1>Redux Counter</h1>
      <div className={classes.value}>{counter}</div>
      <div>
        <button onClick={IncrementHandler}>Increment</button>
        <button onClick={DecrementHandler}>Decrement</button>
      </div>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;
  • 3️⃣ 클래스형 컴포넌트에서 store & reducer function 사용해보기

함수형 컴포넌트가 주로 사용되지만, 그래도 클래스로 할줄도 알아겠죵
훅을 사용할 수 없기 때문에, connect 함수Redux store와 연결 후, 상태dispatch 함수를 사용한다.

class Counter extends Component {
  // this.props.increment()를 호출하여 increment 액션을 디스패치
  IncrementHandler = () => {
    this.props.increment();
  };
  //this.props.decrement()를 호출하여 decrement 액션을 디스패치
  DecrementHandler = () => {
    this.props.decrement();
  };

  render() {
    return (
      <main className={classes.counter}>
        <h1>Redux Counter</h1>
        <div className={classes.value}>{this.props.counter}</div>
        <div>
          <button onClick={this.IncrementHandler.bind(this)}>Increment</button>
          <button onClick={this.DecrementHandler.bind(this)}>Decrement</button>
        </div>
        <button onClick={this.toggleCounterHandler}>Toggle Counter</button>
      </main>
    );
  }
}

// Redux 스토어의 상태를 컴포넌트의 props로 매핑 -> useSelector 대체
const mapStateToProps = (state) => {
  return {
    counter: state.counter,
  };
};
//액션을 디스패치하는 함수 -> useDispatch 대체
const mapDispatchToProps = (dispatch) => {
  return {
    increment: () => dispatch({ type: 'increment' }),
    decrement: () => dispatch({ type: 'decrement' }),
  };
};

// connect 함수로 Counter 컴포넌트를 Redux 스토어에 연결
export default connect(mapStateToProps, mapDispatchToProps)(Counter);

4️⃣ action payload 를 통해, 상태 업데이트에 필요한 데이터를 추가로 포함시키기

// 일반적인 action 객체 구조 
dispatch ({
  type: 'ACTION_TYPE',
  payload: /* 추가 데이터 */
})

보통 action 안에는, 변경 방식인 액션의 type 과 추가적인 정보를 포함할 수 있는 payload를 전달한다.
이를 통해, 유연한 state 변경을 할 수 있는데, 예를 들어 counter 를 1씩 증가시키는게 아니라 버튼에 따라 +5를 증가할 수도 있고, +10 을 할 수 있는 reducer 기능을 만든다고 생각해보자!

Reducer 함수

function counterReducer(state = { counter: 0 }, action) {
  if (action.type === 'increse') {
    return {
      counter: state.counter + action.payload.amount,
    };
  }

  return state;
}

Counter.jsx 에서 dispatch 방법

  const IncreseHandler = () => {
    dispatch({ type: 'increse', payload: { amount: 5 } });
  };

다음과 같이 action 인자를 넘겨주면, action.payload.amount 에 증가하고 싶은 수를 리듀서 함수에 전달할 수 있다!!

그치만, Redux 사용에는 잠재적인 위험이 있다고요 ⚠️

1️⃣ action type 오타 이슈 + 충돌 이슈
식별자 사용시 오타가 발생하면 제대로된 리듀서의 처리가 이루어질 수 없으며,
서로 다른 액션이 많을때 식별자에 대해 충돌이 발생할 수 있다 💥

1️⃣ 관리해야 하는 state 가 많아질 경우 파일일 비대해짐
state 변경시, Reducer Function 안에서 기존의 상태를 복사한 것을 토대로 완전히 새로운 객체를 만들어내면서 상태를 업데이트했다. 근데, 이제 다루는 데이터의 양이 많아진다면, Reducer 파일이 엄청나게 거대해질 거다!!

-> 이때 등장하는 구세주, Redux Toolkit!


Redux toolkit

일단 기본적으로 설치 하시고용

npm i @reduxjs/toolkit

1️⃣ storereducer function 만들기

🔍 state와 reducer 함수를 만드는 방법!
createSlice : Redux 상태 슬라이스를 만드는 함수로, 내부엔 name과, initialState, reducers 함수를 정의한다.

기본형태

//createSlice 기본 형태
const Slice = createSlice({
  name: 'counter', // 슬라이스 이름
  initialState: { counter: 0, ... }, // 초깃값
  reducers: { // 상태를 변경할 리듀서 함수
    increment(state) {
      state.counter++; 
    },
    },
  },
});

어랏 근데, reducer 함수를 보면 상태를 직접적으로 수정하는 것처럼 보이는데, 사실은 toolkitimmer 라이브러리를 사용하기 때문에 가능한 것이다~

immerreducer 함수가 호출될때, 기존 상태에 대한 복사본(드래프트)을 자동으로 생성하며,
드래프트가 변경되면 immer가 이를 기록하고,
reducer 함수 종료시, 변경기록을 바탕으로 새로운 불변 상태 객체를 생성한다!!!

⭐️ 직접 상태를 변경하는 것처럼 보이지만 실제로는 불변상태를 유지하고 있단 말!! ⭐️
덕분에 reducer 함수가 간결해지고, 불변성을 직접 관리해야 하는 번거로움을 줄여줌

🔍 store 를 설정하는 방법!
configureStore : Redux store 설정을 단순화 해주는 함수
리듀서 결합이 매우 쉽다는 점이 특징이다! redux에서 저장소는 단 하나이기 때문에, configureStore를 통해 만들어지는 저장소도 단 하나가 되어야 한다

//configureStore기본 형태
const store = configureStore({
  reducer: counterSlice.reducer,
});

// 여러개의 리듀서를 결합한 경우
const store = configureStore({
  reducer: {
    counter: counterSlice.reducer, 
    user: userSlice.reducer, 
  },
});

store -> index.js

import { createSlice, configureStore } from '@reduxjs/toolkit';

const initialState = { counter: 0, showCounter: true };

// redux 상태 슬라이스를 통햐 state와 reducer 함수를 설정할 수 있음.
// 1) name은 슬라이스의 이름으로 자유롭게 설정가능
// 2) initialState 는 관리할 state의 초깃값을 설정
// 3) reducers 에는 상태를 변경하는 방법을 정의하는 객체로, 내부에 선언된 각 리듀서 함수는
// 인자로 state와 action을 받아서, 새로운 상태를 반환!!
const counterSlice = createSlice({
  name: 'counter',
  initialState: initialState,
  reducers: {
    increment(state) {
      state.counter++;
    },
    increse(state, action) {
      state.counter = state.counter + action.payload.amount;
    },
    decrement(state) {
      state.counter--;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    },
  },
});

const store = configureStore({
  reducer: counterSlice.reducer,
});

export default store;

2️⃣ action을 전달하는 방법!

이전에는 if 문이나 switch 문으로 action type에 따라 reducer 함수에서 다른 동작을 실행했지만, createSlice에서는 그렇지 않다!

1) 액션 만들기 🌱

createSlicereducer 함수에 대해서 고유한 액션 identifier 를 자동으로 생성해준다! 그렇기 때문에 counterSlice.action 안에, 객체로 우리가 만든 reducer 함수가 key 로 들어가 있다!

counterSlice.actions.increment 이렇게 사용하면 액션 객체가 자동으로 생성되어, 우린 이걸 그저 export 해서 dispatch 하는 곳(Component)에서 사용하면 된다!

export const counterActions = counterSlice.actions;

2) dispatch에 action 넣기! 🌱

위와 같이 export 한 값 import 해주고, 불러온 객체값(ex. counterActions)에서 key 값에 접근하면 된다.

요렇게 createSlice 가 만들어준 action 객체 덕분에 오타날 걱정이 없단 말씀!

근데 이제, 추가적으로 payload 하고 싶은 값이 있다면, 호출한 메소드에 인자로 데이터를 넣어서 전달해주면 된다! 전달할 데이터는 객체, 숫자, 문자 상관없다! (전달되어서는 payload 안에 자동으로 들어감)

  const IncreseHandler = () => {
    dispatch(counterActions.increase({ amount: 5 }));
    // dispatch({type: increase, payload:{ amount: 5} }); 와 동일!!!!

3️⃣ 다중 슬라이스 다뤄보기!

한 파일에 모든 슬라이스를 넣으면 파일 부피가 커지기 때문에, slice 별로 파일을 나누는 것이 좋다고 한다!

각 slice 안에는, createSlice 해서 만들어진 state와 Reducer 함수를 포함하며 export 할때는 reducer를 내보내주면 된다. 짜피 store 설정할때 reducer를 전달해줘야 하니까!!

// store -> auth-slice.js
import { createSlice } from '@reduxjs/toolkit';

const initialAuthState = {
  isAuthenticated: false,
};
const authSlice = createSlice({
  name: 'authentication',
  initialState: initialAuthState,
  reducers: {
    login(state) {
      state.isAuthenticated = true;
    },
    logout(state) {
      state.isAuthenticated = false;
    },
  },
});

// store에 연결할 reducer 함수 export 
export const authReducer = authSlice.reducer;
// dispatch 시 사용할, action export 
export const authActions = authSlice.actions;

그럼 이러한 slice 에서 나온 reducer 를 index.js 에 모아 store에 연결해주면 된다.

// store -> index.js
import { configureStore } from '@reduxjs/toolkit';
import { authReducer } from './auth-slice';
import { counterReducer } from './counter-slice';

const store = configureStore({
  reducer: {
    counter: counterReducer,
    auth: authReducer,
  },
});

export default store;

이로써 관리할 데이터가 늘어 파일 크기가 비대해지는 경우를 막을 수 있다!!

0개의 댓글