[React] Redux 시작하기

seung·2022년 6월 10일
0

설치


빈 프로젝트 폴더에서 npm init 실행하여 설정해주고, redux를 설치한다.

$ npm init
$ npm install redux 

자바스크립트에서 리덕스 사용하기


일단 js파일에서 리덕스를 불러와야 한다.

보통 패키지를 불러올때 import를 사용했지만 여기서는 조금 다르게 require를 사용한다.

const redux = require('redux');

다음으로는 스토어와 리듀서를 생성해준다.

아래에 입력해준 리듀서는 상태값의 counter+1 를 해서 객체로 반환해주는 함수이다.

만들어준 counterReducer함수를 createStroe의 매개변수로 담아 스토어로 생성해준다.

→ 어떤 리듀서가 그 저장소를 변경하는지 알아야하기 때문!

const redux = require('redux');

const counterReducer = (state, action) => {
  return {
    counter: state.counter + 1,
  };
};

const store = redux.createStore(counterReducer);

그 다음엔 구독 함수를 생성해주고 리덕스가 이 구독 함수를 인식하도록 하고 상태가 변화할 때 마다 실행하라고 말해줘야 한다.

그러기 위해 store.subscribe() 함수를 통해 구독 함수를 스토어에 전달한다.

const redux = require('redux');

const counterReducer = (state, action) => {
  return {
    counter: state.counter + 1,
  };
};

const store = redux.createStore(counterReducer);

const counterSubscriber = () => {
  // 최신 상태 상태값이 변경될때마다 트리거되고, 스토어의 상태값을 받을 수 있다.
  const latestState = store.getState();
  console.log(latestState);
};

store.subscribe(counterSubscriber);

이 상태로 node로 파일을 실행해보면 에러가 나는 것을 볼 수 있다.

state.counter 가 undefined라서 읽을 수 없다는 타입 에러이다.

이 에러가 나는 이유는 저장소를 생성할 때 해당 리듀서 함수가 실행이 되는데, 이때 그 시점에 state가 정의되어 있지 않기 때문이다.

그래서 해결 방법은 리듀서 함수의 매개변수 state 값에 초기 값을 할당해주는 것이다.

const counterReducer = (state = { counter: 0 }, action) => {
  return {
    counter: state.counter + 1,
  };
};

구독 함수 설정과 리듀서 state의 초기값 설정이 모두 끝났으면 해당 리듀서 함수를 실행시키기 위해 액션을 dispatch로 전달한다.

아래의 코드를 node로 실행하게 되면 { counter: 2 } 라는 결과 값을 받게 된다.

store를 생성할 때 리듀서가 한 번 실행이 되어 counter가 1이 되고,

dispatch로 액션을 전달하여 리듀서가 또 실행이 되어 counter가 2가 되는 것이다.

const redux = require('redux');

const counterReducer = (state = { counter: 0 }, action) => {
  return {
    counter: state.counter + 1,
  };
};

const store = redux.createStore(counterReducer);

const counterSubscriber = () => {
  const latestState = store.getState();
  console.log(latestState);
};

store.subscribe(counterSubscriber);

store.dispatch({ type: 'increment' });

리듀서 내부에서 다른 기능 동작하기

우리는 리듀서를 실행시킬 때 액션을 디스패치로 전달해줬는데, 위에서는 type을 increment로 지정을 해주었다.

그런데 위의 코드는 어떠한 type값이 와도 counter를 1증가 시켜주는 동작만 하게 된다.

다른 동작을 하도록 하고 싶다면 액션의 타입에 따라 다른 기능이 수행되도록 하면 된다.

만약 증가 기능과 더불어 감소 기능도 필요하다면 리듀서 내부에서 조건에 맞게 분기 처리를 해주면 된다.

const 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;
};

리듀서를 위와 같이 수정해주고 아래의 코드를 실행해주면, counter는 순서대로 1, 1, 2 가 출력되는 것을 볼 수 있다.

store.dispatch({ type: 'increment' });
store.dispatch({ type: 'increment' });
store.dispatch({ type: 'decrement' });

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


리덕스는 리액트에서만 사용할 수 있는 것이 아니다.

위의 예제에서 처럼 자바스크립트 파일에서 사용할 수도 있고 다른 언어들에서도 사용할 수 있다.

리액트와 리액트 앱의 작업을 쉽게 하기 위해서 react-redux 패키지도 함께 설치해준다.

이 패키지는 리액트 앱과 리덕스 스토어와 리듀서에 간단하게 연결해줄 수 있게 한다.

$ npm install redux react-redux

src 폴더 내에 store 폴더를 생성해주는데, 이 폴더에는 리덕스에 관련된 코드 파일을 저장한다.

위의 자바스크립트에서 작성했던 예제를 리액트 프로젝트에서 사용하는 방식으로 변경해보자.

require로 불러왔던 redux를 import를 통해 불러왔다.

자바스크립트 예제에서는 store에 subscribe을 연결해주고 스토어 파일 내부에서 디스패치를 해주었다.

하지만, 여기에서는 리액트 앱과 리덕스 스토어를 연결해주기 위해 스토어를 export 해준다.

import { createStore } from 'redux';

const 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;
};

const store = createStore(counterReducer);

export default store;

이제 리덕스 스토어를 리액트 앱에 제공하기 위해 컴포넌트 트리의 최상위인 index.js 파일에서 임포트한다.

(💥 src > store 폴더 내부에 있는 index.js 파일이 아님!)

이전에 작성한 스토어를 임포트해주고 App 컴포넌트를 Provider 컴포넌트로 감싼 뒤 store prop으로 전달한다.

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

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

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

store를 사용하고 싶은 컴포넌트 파일로 이동하고 useSelector를 이용하여 스토어에 저장되어 있는 state를 가져온다.

useSelector의 매개변수로 함수가 필요한데, 해당 함수에는 스토어의 state를 받아서 state의 일부를 반환한다.

useSelector를 사용할 때 react-redux가 자동으로 subscribtion을 설정하게 되고

리덕스 스토어에서 데이터가 바뀔 때마다 최신의 state를 받아올 수 있게 한다.

만약 스토어가 변경된다면 useSelector에 연결된 컴포넌트 함수가 다시 실행될 것이다.

import { useSelector } from 'react-redux';

import classes from './Counter.module.css';

const Counter = () => {
  const counter = useSelector((state) => state.counter);

  const toggleCounterHandler = () => {};

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

export default Counter;

내부 컴포넌트에서 액션 디스패치 하기

useDispatch를 임포트하고 const dispatch = useDispatch(); 처럼 변수로 선언해주면 스토어에 액션을 보낼 수 있는 디스패치 함수로 사용할 수 있다.

증가, 감소 함수를 각각 버튼에 이벤트를 걸어주고 내부에서 액션을 dispatch로 보내주면 끝이다.

import { useSelector, useDispatch } from 'react-redux';

import classes from './Counter.module.css';

const Counter = () => {
  const dispatch = useDispatch();
  const counter = useSelector((state) => state.counter);

  const incrementHandler = () => {
    dispatch({ type: 'increment' });
  };
  const decrementHandler = () => {
    dispatch({ type: 'decrement' });
  };

  const toggleCounterHandler = () => {};

  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;


만약 숫자 5씩 증가되는 버튼을 만들고 싶다면 어떻게 해야할까?

액션에 amount라는 추가적인 속성을 입력하여 해당 속성의 크기만큼 숫자가 증가되게 하면 된다.

우리는 숫자 5를 증가시킬 것이기 때문에 액션에 amount: 5 를 추가해주고

리듀서에서는 해당 기능을 동작시킬 액션의 조건을 분기하여 처리한다.

const counterReducer = (state = { counter: 0 }, action) => {
  if (action.type === 'increment') {
    return {
      counter: state.counter + 1,
    };
  }

  if (action.type === 'increase') {
    return {
      counter: state.counter + action.payload.amount,
    };
  }

  if (action.type === 'decrement') {
    return {
      counter: state.counter - 1,
    };
  }

  return state;
};

const store = createStore(counterReducer);

export default store;

const Counter = () => {
  const dispatch = useDispatch();
  const counter = useSelector((state) => state.counter);

  const incrementHandler = () => {
    dispatch({ type: 'increment' });
  };

  const increaseHandler = () => {
    dispatch({ type: 'increase', amount: 5 });
  };

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

  const toggleCounterHandler = () => {};

  return (
    <main className={classes.counter}>
      <h1>Redux Counter</h1>
      <div className={classes.value}>{counter}</div>
      <div>
        <button onClick={incrementHandler}>Increment</button>
        <button onClick={increaseHandler}>Increase by 5</button>
        <button onClick={decrementHandler}>Decrement</button>
      </div>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;


리듀서에서 state 반환

스토어의 리듀서에서는 기존의 state를 변경하지 않고 state를 객체로 생성하여 반환하고 있다.

아래와 같이 기존의 state를 변경해도 문제없이 동작은 된다.

하지만 리덕스의 스토어에서는 절대 원본 state를 변경해선 안된다.

대신 새로운 state 객체를 반환하여 항상 재정의해주어야 한다.

const counterReducer = (state = initialState, action) => {
  if (action.type === 'increment') {

		// 💥 원본 state를 변경하면 안됨!
		// state.counter++;
		// return state;
    
		// ✨ 새로운 state 객체를 생성하여 반환!
		return {
      counter: state.counter + 1,
      showCounter: state.showCounter,
    };
  }
}
profile
🌸 좋은 코드를 작성하고 싶은 프론트엔드 개발자 ✨

0개의 댓글