3/9(목) Redux - 카운터 프로그램 만들어보기

Mindfulness·2023년 3월 10일
0
post-thumbnail

Redux - 카운터 프로그램 만들어보기

1. 모듈 만들기

  • (1) 리덕스로 만들 첫 프로그램 리덕스로 예전에 만들었던 카운터 프로그램을 다시 만들어 보자. useState 로 만들었던 것을 리덕스로 다시 만들기.
  • (2) 첫 모듈 만들기 모듈이란, State의 그룹. 첫 모듈은 카운터 프로그램에 필요한 State들이 모여있는 모듈이 될 것이다. 아래 순서대로 파일을 생성하고 코드를 입력해보자.
    1. modules 폴더에 counter.js 파일을 생성한다.

    2. 코드를 작성한다.

      // src/modules/counter.js
      
      // 초기 상태값
      const initialState = {
        number: 0,
      };
      
      // 리듀서
      const counter = (state = initialState, action) => {
        switch (action.type) {
          default:
            return state;
        }
      };
      
      // 모듈파일에서는 리듀서를 export default 한다.
      export default counter;

      이제 작성한 모듈 안에 있는 구성요소들을 하나씩 자세히 살펴보자.

2. 모듈의 구성요소 살펴보기

  • (1) initialState === 초기 상태값
    // 초기 상태값
    const initialState = {
      number: 0,
    };
    이것은 initialState 이다. 단어 그대로 초기 상태값. 즉, 어떤 State의 초기값을 정해주는 것. 우리가 useState를 사용했을 때 괄호 안에 초기값을 지정해주던 것과 같은 것.
    const [number, setNumber] = useState(0) // < 여기
    위 코드에서만든 State의 초기값은 { } (객체) 이고, 그 안에 number 라는 변수에 초기값 0을 할당해준 것. 초기값은 꼭 객체가 아니어도 된다. 배열이 되어도 되고, 그냥 원시데이터가 돼도 된다. 그리고 객체에도 여러개의 변수를 넣어줄 수 있다.
    // 초기값이 0
    const initialState = 0;
    
    // 초기값이 0이 있는 배열 
    const initialState = [0];
    
    // 초기값이 number = 0, name = 'Jay'인 객체
    const initialState = {
    	number: 0,
    	name: 'Jay'
    };
  • (2) Reducer === 변화를 일으키는 함수 아래 코드를 리듀서 라고 한다. 리듀서란, 변화를 일으키는 함수.
    다시 말해,
    리듀서는 함수다. 이것만 기억해보자.
    // 리듀서 
    const counter = (state = initialState, action) => {
      switch (action.type) {
        default:
          return state;
      }
    };
    우리가 useState()를 사용할 때, number라는 값을 바꾸고 싶으면 setNumber를 사용했다. 아래 코드 처럼 number값을 변경할 수 있었다.
    // 예시 코드
    
    const onClickHandler = () => {
    	setNumber(number + 1); // setState를 이용해서 state 변경
    }
    리덕스에서는 리듀서가 이 역할을 한다. 우리가 “리듀서야 number에 +1를 해줘" 라고 명령하면, 리듀서는 number에 +1을 더해준다. 그래서 변화를 일으키는 함수라고 표현한 것.
    // src/redux/modules/counter.js
    
    // counter 리듀서
    const counter = (state = initialState, action) => {
      switch (action.type) {
        default:
          return state;
      }
    };
    
    export default counter; // 여기
    그리고 리듀서의 인자에 보면 (state = intialState, action) 이라고 되어 있다. 우리는 리듀서 인자 첫번째 자리에서는 state를, 두번째 자리에서는 action 이라는 것을 꺼내서 사용할 수 있다. state = intialState 처럼 stateinitialState를 할당해줘야 하는 것 기억하기.
  • (3) 카운터 모듈을 스토어에 연결하기 지금까지 모듈파일에서 초기 상태값과 리듀서를 작성했다. 이제 이렇게 만든 모듈을 스토어에 연결 시켜야 한다. 아직까진 모듈과 스토어가 각각 따로 분리되어 있는 상태이기 때문에 우리가 만든 State를 스토어에서 꺼낼 수 없다. configStore.js로 이동해서 아래 코드를 추가해보자.
    // src/redux/modules/config/configStore.js
    
    // 원래 있던 코드
    import { createStore } from "redux";
    import { combineReducers } from "redux";
    
    // 새롭게 추가한 부분
    import counter from "../modules/counter";
    
    const rootReducer = combineReducers({
      counter: counter, // <-- 새롭게 추가한 부분
    });
    const store = createStore(rootReducer);
    
    export default store;
    위와 같이 코드를 추가하면, 스토어와 모듈이 연결된다. 이렇게 스토어와 모듈을 연결시키는 코드는 우리가 모듈을 추가할 때마다 똑같이 진행해주면 된다. 근데, 연결을 하긴했지만 코드상으로만 하다보니 이것이 잘 된것인지 눈으로 확인이 안되서 긴가민가하다. 이제 스토어와 모듈을 정상적으로 잘 연결했는지 눈으로 직접 확인해보자.

3. 스토어와 모듈 연결 확인하기

  • (1) useSelector = 스토어 조회 우리가 생성한 모듈을 스토어에 잘 연결했는지 확인하는 방법은 컴포넌트에서 스토어를 직접 조회하면 된다. 컴포넌트에서 리덕스 스토어를 조회하고자 할때는 useSelector라는 ‘react-redux’의 훅을 사용해야 한다. useSelector의 사용법은 아래와 같다.
    // 1. store에서 꺼낸 값을 할당 할 변수를 선언한다.
    const number = 
    
    // 2. useSelector()를 변수에 할당해준다.
    const number = useSelector() 
    
    // 3. useSelector의 인자에 화살표 함수를 넣어준다.
    const number = useSelector( ()=>{} )
    
    // 4. 화살표 함수의 인자에서 값을 꺼내 return 한다. 
    // useSelector를 처음 사용해보는 것이니, state가 어떤 것인지 콘솔로 확인해보자.
    const number = useSelector((state) => {
    	console.log(state)
    	return state
    });
    App.js 컴포넌트로 이동해서 기존에 있던 코드를 모두 지우고, 아래 코드를 입력해보자.
    // src/App.js
    
    import React from "react";
    import { useSelector } from "react-redux"; // import 하기.
    
    const App = () => {
      const counterStore = useSelector((state) => state); // 추가하기.
      console.log(counterStore); // 스토어 조회
    
      return <div></div>;
    }
    
    export default App;
    우리는 컴포넌트에서 스토어를 조회할 때 react-redux에서 제공하는 useSelector 라는 훅을 사용. (매우 중요 🔥) 브라우저를 켜고, 콘솔을 보면 아래 이미지처럼 객체가 보이고, 그 안에 counter 라는 값이 있는 것을 볼 수 있다. 우리가 만든 counter 라는 모듈의 state가 보이는 것을 알 수 있다. 이렇게 화살표 함수에서 꺼낸 state라는 인자는 현재 프로젝트에 존재하는 모든 리덕스 모듈의 state 인 것.

이제 어떤 컴포넌트에서도 접근 할 수 있는 스토어를 가지게 되었다. 만약 우리가 컴포넌트에서 number라는 값을 사용하고자 한다면 아래 코드처럼 꺼내서 사용하면 된다.

const number = useSelector(state => state.counter.number); // 0

🔥리덕스의 흐름 도식화🔥

  1. View 에서 액션이 일어난다.

  2. dispatch 에서 action이 일어나게 된다.

  3. action에 의한 reducer 함수가 실행되기 전에 middleware가 작동한다.

  4. middleware 에서 명령내린 일을 수행하고 난뒤, reducer 함수를 실행한다. (3, 4번은 아직 몰라도 됩니다!)

  5. reducer 의 실행결과 store에 새로운 값을 저장한다.

  6. store의 state에 subscribe 하고 있던 UI에 변경된 값을 준다.

4. counter.js 모듈의 state 수정 기능 만들기 (+ 1 기능 구현해보기)

  • (1) 어떻게 counter.js 모듈에 있는 state의 값을 변경할 수 있을까?

    useState()를 사용해서 number에 +1을 할 때는 setNumber을 이용해서 +1을 해주었다.

    // 예시 코드
    
    // local state
    const [number, setNumber] = useState(0)
    
    // click handler
    const onClickHandler = () => {
    	setNumber(number + 1)
    }

    리덕스에서의 값의 수정은 리듀서에서 일어난다.

    그럼 만약에 counter.js 모듈에 있는 number에 +1을 하고 싶으면 어떻게 해야할까?

    아래와 같이 해야 합니다.

    1. 리듀서에게 보낼 number를 +1 하라는 “명령”을 만든다.
    2. 명령을 보낸다.
    3. 리듀서에서 명령을 받아 number +1을 한다.
  • (2) 리듀서에게 보낼 “명령” 만들기

    우선 리듀서에게 number에 +1을 하라고 명령을 보내야한다. 명령을 보내기 전에 ‘명령'을 만들어야 한다. 리덕스에서는 그 명령을 Action 이라고 한다. 즉, 리듀서에게 내가 어떤 Action을 하길 원한다. 라고 표현하는 것. 행동을 코드로 나타내면 객체 로 만든다. 그래서 이것을 액션 객체 라고 한다.

    액션 객체는 반드시 type이라는 key를 가져야 한다. 왜냐하면 우리가 이 액션 객체를 리듀서에게 보냈을 때 리듀서는 객체 안에서 type이라는 key를 보기 때문.

    // 예시 코드
    //number에 +1 을 하는 액션 객체
    
    { type : "PLUS_ONE" };

    앞으로 우리는 리덕스 모듈에 있는 state을 변경하기 위해서는 그에 해당하는 액션 객체를 모두 만들어줘야 한다.

  • (3) “명령”(액션 객체) 보내기

    이제 명령을 만들었으니, 우리는 리듀서에게 명령을 보내야 한다. 조금 더 정확하게 표현하게 위해서 지금부터는 “명령"이 아닌 액션객체라고 표현하겠습니다.

    액션객체를 보내기 리듀서로 보내기 위해서는 새로운 훅을 사용해야 한다. 그 훅은 useDispatch라는 훅. react-redux에서 import 해서 사용할 수 있으며, 우리가 만든 액션 객체를 리듀서로 보내주는 역할을 하는 훅이다.

    useDispatch라는 훅을 사용하기 위해서는 컴포넌트 안에서 아래와 같이 먼저 코드를 작성해서 dispatch라는 변수를 생성해줘야 한다. 이렇게 생성한 dispatch는 함수 라는 점 기억하기! 그래서 우리는 dispatch를 사용할 때 () 를 붙여서 함수를 실행하게 된다.

    // src/App.js
    
    import React from "react";
    import { useDispatch } from "react-redux"; // import 하기.
    
    const App = () => {
      const dispatch = useDispatch(); // dispatch 생성
      return (
        <div>
          <button>+ 1</button> {/* 버튼 하나 추가. */}
        </div>
      );
    };
    
    export default App;

    그리고 dispatch를 사용할 때 ( ) 안에 액션객체를 넣어주면 된다. 만약 어떤 버튼을 클릭했을 때 리듀서로 액션객체를 보내고 싶다면 아래와 같이 코드를 작성한다.

    // src/App.js
    
    import React from "react";
    import { useDispatch } from "react-redux"; // import 하기.
    
    const App = () => {
      const dispatch = useDispatch(); // dispatch 생성
      return (
        <div>
          <button
    				// 이벤트 핸들러 추가
            onClick={() => {
    					// 마우스를 클릭했을 때 dispatch가 실행되고, ()안에 있는 액션객체가 리듀서로 전달된다.
              dispatch({ type: "PLUS_ONE" }); 
            }}
          >
    				+ 1
          </button>
        </div>
      );
    };
    
    export default App;

    이렇게 우리는 디스패치를 이용해서 액션객체를 리듀서로 보낼 수 있다. 만약에 어떤 컴포넌트가 렌더링 됐을 때 액션객체를 리듀서로 보내고 싶다면 어떻게 해야 할지도 고민해보기.

  • (4) 액션 객체 받기

    우리가 (3)에서 리듀서로 액션을 보냈다 액션객체를 리듀서로 보냈으니, 리듀서에서 액션객체가 잘 왔는지 확인해보자.

    아마 현재 counter.js 모듈의 코드가 이런 상태 일 것. 코드를 수정해볼 건데. App.js에서 보낸 액션객체를 받을 수 있도록 구현해보자.

    // src/redux/modules/counter.js
    
    // 초기 상태값
    const initialState = {
      number: 0,
    };
    
    // 리듀서
    const counter = (state = initialState, action) => {
      switch (action.type) {
        default:
          return state;
      }
    };
    
    // 모듈파일에서는 리듀서를 export default 한다.
    export default counter;

    리듀서에 action을 콘솔로 찍어보면.

    // src/redux/modules/counter.js
    
    // 초기 상태값
    const initialState = {
      number: 0,
    };
    
    // 리듀서
    const counter = (state = initialState, action) => {
    	console.log(action); // 여기에 console.log(action) 추가
      switch (action.type) {
        default:
          return state;
      }
    };
    
    // 모듈파일에서는 리듀서를 export default 한다.
    export default counter;

    그리고 다시 App.js로 돌아가서 버튼을 눌러서 버튼을 클릭해보자. counter.js 에서 콘솔로 찍은 action이 보이는 것을 확인할 수 있다. 리듀서있는 action은 우리가 App.js에서 dispatch로 보낸 그 액션 객체임을 알 수 있다.

  • (5) 액션객체 명령대로 리듀서가 state값을 변경하는 코드 구현하기

    이제 액션객체를 잘 보내고, 잘 받고 있음을 확인했으니 state에 있는 number를 실제로 변경하는 로직 코드를 구현해보자. 로직코드는 리듀서 안에 있는 스위치문으로 작성된다.

    리듀서가 액션객체를 받아 상태를 바꾸는 원리는 아래와 같다.

    1. 컴포넌트로부터 dispatch를 통해 액션객체를 전달 받는다.

    2. action 안에 있는 type을 스위치문을 통해 하나씩 검사해서, 일치하는 case를 찾는다.

    3. type과 case가 일치하는 경우에, 해당 코드가 실행되고 새로운 state를 반환(return) 한다.

    4. 리듀서가 새로운 state를 반환하면, 그게 새로운 모듈의 state가 된다.

      우리가 (4)에서 1. 컴포넌트로부터 dispatch를 통해 액션객체를 전달 받는다. 까지 구현했으니, 2~4를 한번 직접 코드로 작성해보자.

      // src/modules/counter.js
      
      // 초기 상태값
      const initialState = {
        number: 0,
      };
      
      // 리듀서
      const counter = (state = initialState, action) => {
        console.log(action);
        switch (action.type) {
      		// PLUS_ONE이라는 case를 추가한다.
      		// 여기서 말하는 case란, action.type을 의미한다.
      		// dispatch로부터 전달받은 action의 type이 "PLUS_ONE" 일 때
      		// 아래 return 절이 실행된다. 
          case "PLUS_ONE":
            return {
      				// 기존 state에 있던 number에 +1을 더한다.
              number: state.number + 1,
            };
      
          default:
            return state;
        }
      };
      
      // 모듈파일에서는 리듀서를 export default 한다.
      export default counter;

      action이 {type: “PLUS_ONE”} 이기 때문에, 리듀서 안에 있는 스위치문은 action.type을 조회한다. 그리고 그것이 일치하면 return 절이 실행되고 새로운 state를 반환한다. 막상 글로 보면 복잡해보였지만, 코드로 보면 스위치문에 case를 하나 추가해주는 것으로 완료된다.

      이제 우리가 dispatch를 통해 액션객체를 App.js 컴포넌트에서 보냈고, 그것을 리듀서에서 받아 스위치문을 통해 조건을 찾았고, 그에 해당했을 때 state값을 변경하는 로직까지 모두 구현했으니 정말 state가 잘 변경되는지 확인해보자.

  • (6) useSelector로 변경된 state값 확인하기

    우리가 전 챕터에서 배운 useSelector를 이용해서 App.js에서 state값을 조회해보자. App.js로 이동해서 useSelector를 작성하고, number를 콘솔에 찍어보자. 그리고 number를 화면에도 렌더링 해보자.

    // src/App.js
    
    import React from "react";
    import { useDispatch, useSelector } from "react-redux";
    
    const App = () => {
      const dispatch = useDispatch();
    
    	// 👇 코드 추가
      const number = useSelector((state) => state.counter.number); 
    
      console.log(number); // 콘솔 추가
      return (
        <div>
    			{/* 👇 코드 추가 */}
          {number}
          <button
            onClick={() => {
              dispatch({ type: "PLUS_ONE" });
            }}
          >
            + 1
          </button>
        </div>
      );
    };
    
    export default App;

    우리가 의도한대로 버튼을 누를때마다 number가 1씩 증가는 것을 화면에서도, 콘솔에서도 볼 수 있다. 그리고 리듀서에 작성해둔 console.log(action) 으로 인해 우리가 버튼을 클릭할 때마다 우리가 디스패치한 액션도 볼 수 있다.

    더불어, useState에서 만든 state가 변경되면 화면이 리렌더링되는 것과 마찬가지로, 리덕스에 존재하는 state도 값이 변경되면 useSelector를 하고 있는 컴포넌트들도 모두 다시 리렌더링된다. 그래서 0 → 1 → 2 와 같이 증가하는 것이 보인다.

    5. - 1 기능 빠르게 구현해보기

빼기 1 기능도 위와 동일한 과정으로 구현한다. 아래 코드를 통해 한번 구현해보자.

  • (1) -1 버튼 구현 빼기 버튼을 추가 해보자.
    // src/App.js
    
    import React from "react";
    import { useDispatch, useSelector } from "react-redux";
    
    const App = () => {
      const dispatch = useDispatch();
      const number = useSelector((state) => state.counter.number);
    
      return (
        <div>
          {number}
          <button
            onClick={() => {
              dispatch({ type: "PLUS_ONE" });
            }}
          >
            + 1
          </button>
    
          {/* 빼기 버튼 추가 */}
          <button>
            - 1
          </button>
        </div>
      );
    };
    
    export default App;
  • (2) 액션객체 디스패치하기 버튼의 이벤트핸들러에 dispatch와 액션객체를 넣어주자.
    // src/App.js
    
    import React from "react";
    import { useDispatch, useSelector } from "react-redux";
    
    const App = () => {
      const dispatch = useDispatch();
      const number = useSelector((state) => state.counter.number);
    
      return (
        <div>
          {number}
          <button
            onClick={() => {
              dispatch({ type: "PLUS_ONE" });
            }}
          >
            + 1
          </button>
          <button
            onClick={() => {
    					// 액션객체 디스패치
              dispatch({ type: "MINUS_ONE" });
            }}
          >
            - 1
          </button>
        </div>
      );
    };
    
    export default App;
  • (3) 리듀서 로직 구현 스위치문에 case를 추가해보자.
    // src/modules/counter.js
    
    // 초기 상태값
    const initialState = {
      number: 0,
    };
    
    // 리듀서
    const counter = (state = initialState, action) => {
      switch (action.type) {
        case "PLUS_ONE":
          return {
            number: state.number + 1,
          };
    
    		// action.type이 MINUS_ONE 일 때 새로운 state 반환
        case "MINUS_ONE":
          return {
            number: state.number - 1,
          };
        default:
          return state;
      }
    };
    
    // 모듈파일에서는 리듀서를 export default 한다.
    export default counter;

6. 정리

  • 액션객체란, 반드시 type이란 key를 가져야 하는 객체이다. 또한 리듀서로 보낼 “명령"이다.
  • 디스패치란, 액션객체를 리듀서로 보내는 “전달자” 함수이다.
  • 리듀서란, 디스패치를 통해 전달받은 액션객체를 검사하고, 조건이 일치했을 때 새로운 상태값을 만들어내는 “변화를 만들어내는" 함수이다.
  • 디스패치(dispatch)를 사용하기위해서는 useDispatch() 라는 훅을 이용해야 한다.
    • 디스패치는 스토어의 내장함수 중 하나입니다.
    • 우선, 디스패치는 액션을 발생 시키는 것 정도로 이해하시면 됩니다.
    • dispatch 라는 함수에는 액션을 파라미터로 전달합니다.. dispatch(action) 이런식으로 말이죠.
  • 액션객체 type의 value는 대문자로 작성한다. (JS에서 상수는 대문자로 작성하는 룰이 있음)
profile
Junior Frontend Developer

0개의 댓글