react-redux

김동현·2022년 3월 14일
0

Redux

목록 보기
3/5
post-thumbnail

React App에서 Redux 사용하기

Redux 라이브러리는 React에 국한된 라이브러리가 아니며 어떤 자바스크립트 프로젝트에서도 사용 가능한 라이브러리 입니다.

redux와 react-redux 라이브러리 설치하기

우리는 redux와 react app의 작업을 쉽게 하기 위해서 redux 패키기 뿐만 아니라 "react-redux" 패키지도 같이 설치합니다.

npm install redux react-redux

React App에서 Redux 사용시 "react-redux 패키지"가 유용한 기능들을 제공해줍니다.

redux store 생성하기

아래는 앞에서 작성했던 redux 관련 로직을 react로 옮겨 작성했습니다.

import { createStore } from 'redux';

  // 초기 상태 객체
const initialState = { counter: 0 };

  // reducer 함수
const reducer = (stateObj = initialState, action) => {
    if (action.type === 'increment') {
        return { counter: stateObj.counter + 1 };
    }
    
    if (action.type === 'decrement') {
        return { counter: stateObj.counter - 1 };
    }
    
    return stateObj;
};

  // store 생성
const store = createStore(reducer);

export default store;

React App과 Redux store을 서로 연결하기 위해서는 생성한 store을 React App에 제공해야 합니다.

참고로 애플리케이션은 단 하나의 store 객체와 연결할 수 있습니다.

React App과 redux store 연결하기

store을 React App에 제공하기 위해서 일반적으로는 컴포넌트 트리의 최상위 노드인 루트 컴포넌트(App 컴포넌트)를 "react-redux" 라이브러리의 "Provider 컴포넌트"로 감싸줍니다.

그리고 생성한 store을 Provider 컴포넌트의 "store" 어트리뷰트에 작성함으로써 redux store와 React App이 서로 연결됩니다.

// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

  // store와 React App 연결을 위한 컴포넌트
import { Provider } from 'react-redux';

  // 생성한 store 객체
import store from './store/index';

const rootNode = ReactDOM.createRoot(document.getElementById('root'));

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

useSelector 훅으로 store의 상태값 가져오기

컴포넌트가 redux store의 상태 객체의 특정 상태값을 추출하기 위해서 "react-redux" 라이브러리가 제공하는 커스텀 훅인 "useSelector 훅"을 사용합니다.

useSelector인수로 콜백 함수를 전달하며, 콜백 함수는 인수로 기존 상태 객체를 전달받고 반환값으로 작성된 상태값을 반환합니다.

import { useSelector } from 'react-redux';

const state = useSelector(stateObj => stateObj.state);

useSelector 훅은 두 가지 동작을 합니다.

  1. redux store 상태 객체의 "일부 상태값만 추출"하도록 도와줍니다.

  2. useSelector 훅으로 추출한 상태값이 "변경"된 경우 react-reudx가 자동적으로 "해당 컴포넌트를 재평가하고 리렌더링"되도록하는 subscription을 subscribe 해줍니다.

useSelector 훅을 사용하여 가져온 상태값이 변경된 경우 react-redux가 그 상태를 사용하고 있는 컴포넌트를 재평가하고 리렌더링하는 subscription을 실행합니다.
이는 컴포넌트를 재평가하고 리렌더링되도록 하는 subscription을 useSelector 훅이 자동적으로 subscribe 해주기 때문입니다.

만약 reducer 함수가 반환한 새로운 객체의 상태값이 이전 객체의 상태값과 일치하다면(=== 연산자를 통한 단순 비교) 해당 상태는 변경되지 않은 것으로 동작합니다. 일치하지 않는 경우 상태가 변경되지 않았으므로 subscription 또한 실행되지 않습니다. 즉, 컴포넌트가 재평가되지 않습니다.

// Counter.js
import { useSelector } from 'react-redux';

const Counter => {
    // 상태 객체의 counter 상태값 추출
    // Counter 컴포넌트는 counter 상태값 변경시 재평가되고 리렌더링
    const counter = useSelector(state => state.counter);
    
    return (
        <main>
            <h1>Redux Counter</h1>
            <div>{counter}</div>
        </main>
    );
}

export default Counter;

useDispatch 훅으로 dispatch 함수 가져오기

action 객체를 reducer 함수에게 전달하기 위해서는 dispatch 함수가 필요합니다. "react-redux" 라이브러리의 "useDispatch 훅" 호출시 dispatch 함수를 반환합니다.

useDispatch 훅을 호출하면 reducer에 action을 보내는 dispatch 함수를 반환합니다.

dispatch 함수에는 일반적으로 type 프로퍼티가 존재하는 객체(action)를 전달하면서 호출합니다. action.type의 값은 reducer 함수에서 사용되는 식별자 역할을 합니다. type 프로퍼티 값은 다른 값들과는 충돌해서는 안됩니다. 즉, reducer 함수에 전달되는 각 action 객체의 type 프로퍼티 값들은 중복되어서는 안되며 유일한 값을 가져야 합니다.

// Counter.js
import { useSelector, useDispatch } from 'react-redux';

const Counter => {
    const counter = useSelector(state => state.counter);
    
    // dispatch 함수 반환
    const dispatch = useDispatch();
    
    const incrementHandler = () => {
        // type 프로퍼티를 갖는 action 객체를 dispatch 함수 인수로 전달하면서 호출
        // action 객체는 reducer 함수에게 전달되면서 호출
        dispatch({ type: 'increment' });
    };
    
    const decrementHandler = () => {
        // type 프로퍼티를 갖는 action 객체를 dispatch 함수 인수로 전달하면서 호출
        // action 객체는 reducer 함수에게 전달되면서 호출
        dispatch({ type: 'decrement' });
    };
    
    return (
        <main>
            <h1>Redux Counter</h1>
            <div>{counter}</counter>
            <div>
                <button onClick={incrementHandler}>increment</button>
                <button onClick={decrementHandler}>decrement</button>
            </div>
            <button>Toggle Counter</button>
        </main>
    );
}

export default Counter;

connect 함수로 state와 dispatch 가져오기

"react-redux" 라이브러리의 "connect 함수"를 통해 상태 객체의 일부 상태값과, dispatch 함수를 호출하는 메서드를 props로 컴포넌트가 전달받을 수 있습니다.

connect 함수는 다음과 같은 형식으로 사용합니다.

import { connect } from 'react-redux';

  // 상태값을 props로 전달해주는 함수
const mapStateToProps = stateObj => {
    
    // 프로퍼티들이 props 객체의 프로퍼티로 전달
    return { stateName: stateObj.state, ,,, };
};

  // dispatch 호출문을 갖는 메서드를 props으로 전달해주는 함수
const mapDispatchToProps = dispatch => {

    // 프로퍼티들이 props 객체의 프로퍼티로 전달
    return { methodName() { dispatch(action) },,, };
};

export default connect(mapStateToProps, mapDispatchToProps)(Component);

1. mapStateToProps

"mapStateToProps" 함수는 store의 상태 객체로부터 상태값 일부를 추출하여 props 객체의 프로퍼티로 컴포넌트에게 전달해줍니다.

mapStateToProps함수는 두 개의 인수를 전달받을 수 있습니다.

  1. 첫 번째 인수로는 store의 "상태 객체"를 전달받습니다

  2. 두 번째 인수는 옵션으로 컴포넌트가 현재 갖고 있는 모든 "props"를 전달받을 수 있습니다.

그리고 mapStateToProps반환값으로 작성한 객체의 프로퍼티들은 props 객체의 프로퍼티로 전달됩니다. 반환값으로 작성한 객체의 키가 props 객체의 키로, 프로퍼티 값이 props 객체의 프로퍼티의 값으로 전달됩니다.

즉, useSelector 훅의 인수로 전달하는 콜백함수를 통해 store 내부 데이터를 가져오는 동작과 유사합니다.

// 인수로 상태 객체를 전달받음
const mapStateToProps = stateObj => {
    return {  
        // 추출할 상태값을 컴포넌트에게 전달될 props 객체의 프로퍼티로 작성
        counter: stateObj.counter
    };
}

connect 함수 또한 useSelector 훅처럼 상태값이 변경된 이후에 컴포넌트를 재평가하는 subscription이 실행됩니다.

2. mapDispatchToProp

mapDispatchToProp도 함수입니다. 이 함수는 reducer 함수를 호출하도록 하는 dispatch 호출문을 갖는 메서드들을 갖는 객체를 반환값에 작성하여 각 메서드들을 props로 컴포넌트에게 전달해주는 함수입니다.

mapDispatchToProp 함수는 "인수로 dispatch 함수"를 전달받습니다. 그리고 객체를 반환하는데 객체에는 메서드들을 작성합니다.
반환값에 작성된 객체의 메서드들은 내부에서 dispatch 호출문을 포함하도록 작성합니다. 이렇게 작성된 메서드들은 props 객체의 프로퍼티로 컴포넌트에게 전달됩니다.

// 인수로 dispatch 함수를 전달받음
mapDispatchToProp = dispatch => {
    return {  // 각 메서드들은 props 객체의 프로퍼티로 컴포넌트에게 전달
        increment() {
            // 메서드 내부에서는 dispatch 호출문 작성
            dispatch({ type: 'increment' });
        },
        decreament() {
            // 메서드 내부에서는 dispatch 호출문 작성
            dispatch({ type: 'decreament' });
        }
    };
};

mapDispatchToProps 함수를 통해서 useDispatch 훅을 대체할 수 있습니다.

3. connect(mapStateToProps, mapDispatchToProps)(Component);

connect 함수 인수로 앞에서 정의한 mapStateToPropsmapDispatchToProps 함수를 전달하면 함수가 반환되고, 그 함수의 인수로 상태값과 dispatch 호출문을 갖는 메서드를 props로 전달받을 컴포넌트를 전달합니다.

전달한 컴포넌트가 mapStateToProps 함수가 반환한 객체의 상태(프로퍼티)를 props로 전달받고, mapDispatchToProp 함수가 반환한 객체의 메서드들도 props로 전달받게 됩니다.

Redux 상태를 올바르게 사용하기

redux가 reducer 함수를 실행하여 상태를 변경할 때 절대 기존 상태(reducer 함수가 전달받는 첫 번째 인수)를 직접 변경해서는 안됩니다. 상태 객체는 불변 객체로서 절대 직접적으로 접근하여 변경해서는 안됩니다. 대신에 reducer 함수의 반환값으로 새로운 상태값을 갖는 새로운 상태 객체를 반환하는 방식으로 상태값을 변경할 수 있습니다.

예를 들어, reducer 함수가 반환하는 상태 객체에 reducer 함수가 전달받는 기본 상태 객체를 참조하여 값을 직접 변경할 수 없습니다.

const initialState = { count: 0 };

reducer(state = initialState, action) {
    if (action.type === 'increasment') {
        return {
            count: ++state.count // -> state를 직접 변경해서는 안된다.
        };
    }
    ,,,

위 코드처럼 ++state.count로 직접 state 객체를 변경해서는 안된다. 위 코드는 아래처럼 작성해야 합니다.

const initialState = { count: 0 };

reducer(state = initialState, action) {
    if (action.type === 'increasment') {
        return {
            count: state.count + 1 // -> 새로운 값을 생성하여 작성
        };
    }
    ,,,

우리는 reducer 함수가 전달받는 상태들을 포함한 상태 객체를 함수 내부에서 직접 변경해서 사용해서는 안되며, 반환값으로 언제나 새로운 값을 정의하여 객체의 상태값(프로퍼티 값)을 변경해주어야 합니다.

이후 이전 객체가 갖고 있던 이전 상태값과 반환된 객체의 현재 상태값을 서로 단순 비교하여 일치하지 않은 경우 상태가 변경된 것으로 처리됩니다(이후 subscription 함수 실행).

이때 주의할 점으로 객체의 프로퍼티 값, 즉 객체의 상태값이 객체 타입인 경우 객체 내부 구조와 프로퍼티 값이 동일하더라도 객체 리터럴의 경우 평가할 때마다 새로운 값을 생성하는 점을 주의해야 합니다.

const initialState = { user: { name: 'Kim', age: 25 } };

reducer(state = initialState, action) {
    if (action.type === 'changeName') {
        return {
            // 객체의 구조가 동일하더라도 
            // 이전 user 상태값과 현재 user 상태값이 서로 다른 값을 갖고 있음
            user: {
                name: 'Kim',
                age: 25
            }
        };
    }
    ,,,
profile
Frontend Dev

0개의 댓글