본문 바로가기

Front-End: Web/React.js

[코딩애플] Redux 사용해서 쉽게 state 관리하기!

반응형

Redux 사용 이유

  • 컴포넌트 간 state 공유가 편해진다 -> props 전달할 필요가 없다.

Q. 장바구니 state가 App, Detail, Cart 컴포넌트에 필요하다면 어디에 만들어야할까?

A. 최상단 컴포넌트에. 그런데 props를 일일히 전달해주기 귀찮다. Redux를 사용하면 컴포넌트들이 props 없이 state를 공유할 수 있다.

redux를 설치하고 store.js 파일을 생성해서 그 안에 모든 state를 보관할 수 있고, 모든 state를 어느 컴포넌트에서든 가져다 쓸 수 있다.

사이즈가 큰 프로젝트는 Redux가 필수이므로, 리액트 구인 시 대부분 Redux를 요구하니 알아두자!

Redux Toolkit 설치

설치 전 주의사항

package.json에서 'react'와 'react-dom'이 18.1v 이상인지 확인하자.

Redux 설치하기

npm install @reduxjs/toolkit react-redux

Redux 세팅

세팅1. store.js 파일 생성

🕹️store.js

import { configureStore } from '@reduxjs/toolkit'

export default configureStore({
  reducer: { }
}); 

세팅2. index.js 가서 <Provider store={store}> 쓰기

store.js에서 export한 configureStore를 store명으로 불러오고, index.js에서 렌더링되는 컴포넌트 전체를 <Provider store={store}>로 감싼다.

🕹️index.js

import {Provider} from 'react-redux';
import store from './store.js';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <Provider store={store}>
    	<BrowerRouter>
            <App />
        </BrowerRouter>
    </Provider>
);    

그러면 이제 App와 그의 모든 자식들은 store.js에 있던 state를 전부 사용할 수 있게 된다.

Redux store에 state 보관하는 법

🕹️store.js

import { configureStore } from '@reduxjs/toolkit'

const user = createSlice({ 
    name: 'state이름',
    initialState: '값'
})

export default configureStore({
  reducer: {
      user: user.reducer
  }
}); 
  • **createSlice** : useState의 역할이다
  • state 하나를 slice라고 한다.
  • slice를 만들면 변수로 저장하고, configureStore에 등록한다.

값을 지정해보자.

import { configureStore } from '@reduxjs/toolkit'

const user = createSlice({ 
    name: 'user',
    initialState: 'kim'
})

export default configureStore({
  reducer: {
      user: user.reducer
  }
});

Redux store에 있던 state 가져다 쓰는 법: useSelector

Cart.js에서 store에 있는 값을 가져다 써보자.

🕹️Cart.js

import { useSelector } from 'react-redux';

function Cart(){
    const a = useSelector((state) => state)
    console.log(a); // {user: 'kim'}
}

user만 가져오고 싶으면

const {user} = useSelector((state) => state);
console.log(user); // kim

store.js에서 state를 하나 더 만들어 보자. 재고 stock를 나타내는 state를 만든다.

🕹️store.js

import { configureStore } from '@reduxjs/toolkit'

const user = createSlice({ 
    name: 'user',
    initialState: 'kim'
})

const stock = createSlice({ 
    name: 'stock',
    initialState: [10, 11, 12]
})

export default configureStore({
  reducer: {
      user: user.reducer,
      stock: stock.reducer
  }
});

이제 다시 Cart.js의 출력 결과를 보면, 추가한 state도 같이 나온다.

🕹️Cart.js

import { useSelector } from 'react-redux';

function Cart(){
    const a = useSelector((state) => state)
    console.log(a); // {user: 'kim', stock: Array(3)}
}

stock 부분만 쓰고 싶으면 state.stock로 가져다 쓰면 된다.

(참고) useSelector 편하게 쓰려면

import { useSelector } from 'react-redux';

function Cart(){
    const a = useSelector((state) => state) // ⬅️
}

화살표 부분에서 state는 store안에 있던 모든 state가 된다.

그래서 .을 찍어서 원하는 것만 쉽게 가져다 쓰도록 하자.

import { useSelector } from 'react-redux';

function Cart(){
    const a = useSelector((state) => state.stock) // ⬅️
}

Redux 쓰면 되지 props는 왜 가르친대?

Redux vs props

왜냐하면 Redux를 사용하려면 라이브러리 설치도 해야하고,

세팅하는 문법도 필요하고,

state 만들었으면 등록하는 과정도 필요하고,

...

이처럼 코드가 더 길어질 수 있다.

그래서 간단한 프로젝트의 경우에는 그냥 props를 사용하는 게 코드가 더 짧고 간단하고, 컴포넌트가 많은 경우에는 Redux를 쓰는 게 좋다.

Redux store 안에 모든 걸 넣지 말자

Redux 쓴다고 해서 모든 state를 store에 보관할 필요는 없다.

만약 생성된 state가 한 컴포넌트 내에서만 사용되고 공유될 필요가 없는 경우에는 Redux store에 등록하지 않으면 된다.

Redux store의 state를 변경하는 법: useDispatch

  1. store.js에서 state를 수정해주는 함수를 만들고,
  2. export한다
  3. 원할 때 그 함수를 실행해달라고 store.js에 요청한다. (dispatch(state변경함수()))

1. state 수정해주는 함수 만들기

user의 initialState인 'kim'을 'john'으로 변경해주도록 해보자.

먼저 store.js에서 state를 수정해주는 함수를 만든다.

🕹️store.js

import { configureStore } from '@reduxjs/toolkit'

const user = createSlice({ 
    name: 'user',
    initialState: 'kim',
    reducers: {
        changeName(){
            return 'john'
        }
    }
})

export default configureStore({
  reducer: {
      user: user.reducer,
  }
});

근데 만약 state를 변경할 때 기존 state가 필요하다면?

const user = createSlice({ 
    name: 'user',
    initialState: 'kim',
    reducers: {
        changeName(state){ // ⬅️ 함수에 state를 추가한다.
            return 'john' + state;
        }
    }
})

여러 함수를 만들고 싶다면, reducers 객체 내에 함수를 더 추가한다.

const user = createSlice({ 
    name: 'user',
    initialState: 'kim',
    reducers: {
        changeName(state){
            return 'john' + state;
        },
        다른함수(){
            
        },
        ...
    }
})

2. 만든 함수 export 하기

만든 함수를 다른 컴포넌트에서 쓸 수 있도록 export 해준다.

user.actions를 작성하면 slice 안에 있는 모든 함수를 가져온다. destructuring을 사용해서 더 간편하게 모든 함수를 가져올 수 있다.

🕹️store.js

export let { changeName, 다른함수, ... } = user.actions

3. 만든 함수 import해서 사용하기

버튼을 누르면 state를 john kim으로 변경하려면?

🕹️Cart.js

import { useDispatch } from 'react-redux';
import { changeName } from './../store.js';

function Cart(){ 
    const dispatch = useDispatch();
    
    const onClick = () => {
        dispatch(changeName());
    }
    
    return(
        <>
        	<button onClick={onClick}>+</button>   
        </>)
}
  • **useDispatch**: store.js로 요청을 보내주는 함수
  • 사용 시엔 **dispatch(state변경함수())** 이렇게 한다.
  • changeName()을 이 자리에서 실행해 달라는게 아니라, store.js에게 changeName()을 실행해달라고 부탁하는 메시지를 보낸다.

이렇게 쓰는 게 처음엔 이해가 되지 않고 번거롭겠지만, 프로젝트 사이즈가 커지면 실은 좋은 방식이다. 버그를 잘 방지할 수 있기 때문이다. 생각해보자.

만약 user state 값인 'kim'을 가져다 쓰는 컴포넌트가 10개가 있다고 가정하자. 변경도 자유자제로 할거다. 그러면 갑자기 버그가 나서 'kim'이 갑자기 숫자 123이 됐다고 하자. 그럼 이 버그를 일으킨 범인을 찾아야한다. 하지만 모든 컴포넌트가 사용하고 있으니까 범인을 찾으려면 모든 컴포넌트를 뒤져야한다.

store.js에 state를 수정하는 함수를 미리 만들어놓고 컴포넌트들이 이 수정하는 함수를 부르고 수정해달라고 부탁하는 식으로 state를 변경하게 되면 버그를 추적할 때 훨씬 쉬워진다. 왜냐하면 범인은 무조건 state를 수정하는 store.js이기 때문이다.

state가 object/array일 경우 변경하는 법

state가 object/array인 경우 변경하는게 특이하니까 알아보자.

object/array의 경우, 직접 수정해도 state가 변경된다

initialState를 object로 변경한다.

🕹️store.js

const user = createSlice({ 
    name: 'user',
    initialState: {
        name: 'kim',
        age: 20
    },
    reducers: { }
    }
})

{name: 'park'}으로 바꾸려면?

const user = createSlice({ 
    name: 'user',
    initialState: {
        name: 'kim',
        age: 20
    },
    reducers: {
        changeName(state){
            return {name: 'park', age: 20}
        }
    }
})

이렇게도 되겠지만 더 쉬운 방법이 있다.

state가 object/array인 경우에는 returnn문을 사용하지 않아도 변경이 된다. Immer.js라는 라이브러리가 자동으로 설치되기 때문에 이 도움을 받기 때문이다.

reducers: {
    changeName(state){
        state.name = 'park'; // ⬅️
    }
}

Q. 버튼 누르면 age가 +1 되는 기능?

    reducers: {
        changeName(state){
            state.name = 'park';
        },
        increate(state){
            state.age += 1;
        }
    }
...
export let { changeName, increase } = user.actions;

Q. 원하는 수만큼 age +n 하는 기능? -> state 변경 함수에 파라미터 뚫기

    reducers: {
        changeName(state){
            state.name = 'park';
        },
        increate(state, action){
            state.age += action.payload; // ⬅️
        }
    }
...
export let { changeName, increase } = user.actions;

왜 payload인가?

increase라는 함수를 실행해달라고 store.js에 요청을 하는데, 메시지에 실어 나르는 화물(payload)라 하여 payload라고 작성한다.

왜 action인가?

**state 변경 함수를 action이라고 한다**. payload 뿐만 아니라 action에 관련된 다양한 정보들을 가지고 있기 때문에 action이라 부른다.

 

파일 분할하기

코드가 길어지면 import, export 하여 쓰도록 파일을 분할하자.

user가 길어서 다른 파일로 저장해서 가져다 쓰도록 하자.

🕹️store/userSlice.js

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

const user = createSlice({ 
    name: 'user',
    initialState: {
        name: 'kim',
        age: 20
    },
    reducers: {
        changeName(state){
            return {name: 'park', age: 20}
        },
        increate(state, action){
            state.age += action.payload; 
        }
    }
});

export const { changeName, increase } = user.actions;

export default user;

🕹️store.js

import user form './store/userSlice.js';

그리고 changeName과 increase의 export하는 파일이 바뀌었으니까 가져다 쓰는 곳에서도 from을 변경한다.

🕹️Cart.js

import { changeName, increase } from './../store/userSlice.js';
반응형