천진난만 코딩 스토리

2023.03.04) 항해 27일차 (1-Redux) 본문

TIL(Today I Learned)

2023.03.04) 항해 27일차 (1-Redux)

Wisdom_1104 2023. 3. 4. 22:58

오늘은 리덕스를 배웠다.

새로운 개념이라 긴장하고 좀 두려운 마음으로 리덕스를 접했다.

생각보다는 어떤 건지 감은 일찍 잡을 수 있었지만..............

머릿속에 콕 박히지 않았다.

그래서 중간에 머리를 식힐 겸 드라이브를 하고 와서

다시 보니 조금은 이해가 되긴 했지만

확실하게 이해하기 위해서는 내일 코드를 여러 번 쳐봐야 할 것 같다.

 

 

1) 리덕스란

리덕스란 “중앙 state 관리소”를 사용할 수 있게 도와주는 패키지(라이브러리)이다.

프론트엔드 개발자들은 “리덕스”를 전역 상태관리 라이브러리라고 많이 표현한다.

전역 상태, 즉 Global State를 의미하고 그것을 관리하게 도와주는 라이브러리 (패키지) 이기 때문이다.

 

① useState의 불편함

  • 컴포넌트에서 컴포넌트로 State를 보내기 위해서는 반드시 부-모 관계가 되어야 한다.
  • 조부모 컴포넌트에서 손자 컴포넌트로 값을 보내고자 할 때도 반드시 부모 컴포넌트를 거쳐야만 한다.
  • 자식 컴포넌트에서 부모 컴포넌트로 값을 보낼 수 없다.

이러한 불편함을 개선하는 그 새로운 도구가 바로 리덕스다.

우리가 리덕스를 사용하면 State를 공유하고자 할때 부-모 관계가 아니어도 되고, 중간에 의미 없이 컴포넌트를 거치지 않아도 된다.

자식 컴포넌트에서 만든 State를 부모 컴포넌트에서도 사용할 수 있게 된다.

 

②  Global state와 Local state

  • Local state (지역상태)
    • 컴포넌트에서 useState를 이용해서 생성한 state 다. (좁은 범위 안에서 생성된 State)
  • Global state (전역상태)
    • Global state는 컴포넌트에서 생성되지 않는다.
    • 중앙화된 특별한 곳에서 State들이 생성된다. (중앙 state 관리소)

앞으로 State를 Global state와 Local state라는 것을 따로 구분지어서 표현할 것이다.

 

 

2) 리덕스 설정

yarn add redux react-redux

패키지를 설치하고, 폴더를 생성한다.

  • redux : 리덕스와 관련된 코드를 모두 모아 놓을 폴더
  • config : 리덕스 설정과 관련된 파일들을 놓을 폴더
  • configStore : “중앙 state 관리소"인 Store를 만드는 설정 코드들이 있는 파일
  • modules : 만들 State들의 그룹
// src/configStore.js
import { createStore } from "redux";  //리덕스의 가장 핵심이 되는 스토어를 만드는 메소드(함수)
import { combineReducers } from "redux";//여러 개의 독립적인 reducer의 반환 값을 하나의 상태 객체로 만들어준다

const rootReducer = combineReducers({}); 
const store = createStore(rootReducer); 

export default store; 


//index.js
// 원래부터 있던 코드
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

// 우리가 추가할 코드
import store from "./redux/config/configStore";
import { Provider } from "react-redux";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(

	//App을 Provider로 감싸주고, configStore에서 export default 한 store를 넣어줍니다.
  <Provider store={store}> 
    <App />
  </Provider>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

3) 카운터 프로그램 만들며 설정 이해하기

① 모듈 만들기

// src/modules/counter.js

// 초기 상태값
const initialState = {
  number: 0,
};

// 리듀서
const counter = (state = initialState, action) => {
  switch (action.type) {
    default:
      return state;
  }
};

// 모듈파일에서는 리듀서를 export default 한다.
export default counter;

위 코드에서 만든 State의 초기값은 { } (객체)이고, 그 안에 number라는 변수에 초기값 0을 할당해 준 것이다.

초기값은 꼭 객체가 아니고, 배열, 원시데이터, 여러 개의 변수를 가진 객체도 넣어줄 수 있다.

리듀서란, 변화를 일으키는 함수다.

리듀서의 인자에 보면 (state = intialState, action)이라고 되어 있는데,

리듀서 인자 첫 번째 자리에서는 state를, 두 번째 자리에서는 action이라는 것을 꺼내서 사용할 수 있다.

state = intialState처럼 state에 initialState를 할당해줘야 한다.

 

② 카운터 모듈을 스토어에 연결하기

모듈파일에서 초기 상태값과 리듀서를 작성했고, 이제 만든 모듈을 스토어에 연결시켜야 한다.

아직까진 모듈과 스토어가 각각 따로 분리되어 있는 상태이기 때문에 만든 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;

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

"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
});

이렇게 화살표 함수에서 꺼낸 state라는 인자는 현재 프로젝트에 존재하는 모든 리덕스 모듈의 state 인 것이다.

 

 

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

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

counter.js 모듈에 있는 number에 +1을 하고 싶으면,

  1. 리듀서에게 보낼 number를 +1 하라는 “Action”을 만든다.
  2. Action을 보낸다.
  3. 리듀서에서 Action을 받아 number +1을 한다.

 

Action을 코드로 나타내면 객체로 만들어서 이것을 액션 객체 라고 한다.

액션 객체는 반드시 type이라는 key를 가져야 한다.

액션 객체를 리듀서에게 보냈을 때 리듀서는 객체 안에서 type이라는 key를 보기 때문이다.

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

 

액션 객체 보내기

액션객체를 보내기 리듀서로 보내기 위해서는 useDispatch라는 훅을 사용해야 한다.

react-redux에서 import 해서 사용할 수 있으며, 우리가 만든 액션 객체를 리듀서로 보내주는 역할을 하는 훅이다.

useDispatch라는 훅을 사용하기 위해서는 컴포넌트 안에서 아래와 같이 먼저 코드를 작성해서 dispatch라는 변수를 생성해줘야 한다.

이렇게 생성한 dispatch는 사용할 때 ()를 붙여서 함수를 실행하게 됩니다.

그리고 dispatch를 사용할 때 ( ) 안에 액션객체를 넣어주면 된다.  

// src/App.js

import React from "react";
import { useDispatch, useSelector } from "react-redux"; // import

const App = () => {
  const dispatch = useDispatch(); // dispatch 생성

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

  return (
    <div>
      {number}
      <button
        onClick={() => {
          dispatch({ type: "PLUS_ONE" }); 
          //마우스를 클릭했을 때 dispatch가 실행되고, ()안에 있는 액션객체가 리듀서로 전달된다.
        }}
      >
        + 1
      </button>
    </div>
  );
};

export default App;

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

  • 리듀서가 액션객체를 받아 상태를 바꾸는 원리
  • 컴포넌트로부터 dispatch를 통해 액션객체를 전달받는다.
  • action 안에 있는 type을 스위치문을 통해 하나씩 검사해서, 일치하는 case를 찾는다.
  • type과 case가 일치하는 경우에, 해당 코드가 실행되고 새로운 state를 반환(return) 한다.
  • 리듀서가 새로운 state를 반환하면, 그게 새로운 모듈의 state가 된다.
// 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 Creator

만약에 우리가 액션객체의 value를 변경할 일이 생긴다면 코드에서 모두 찾아서 변경해주어야 한다.

이렇게 하면 오타가 날 수도 있고 번거롭기 때문에 ,

액션객체를 한 곳에서 관리할 수 있도록 “함수"와 액션 value를 상수로 만들 수 있다.

 

// src/redux/modules/counter.js

const PLUS_ONE = "PLUS_ONE"; // value는 상수로 생성

// 액션객체를 반환하는 함수 생성
// export 가 붙는 이유는 plusOne()는 밖으로 나가서 사용될 예정이기 때문이다.
export const plusOne = () => { 
  return {
    type: PLUS_ONE, // type에는 위에서 만든 상수로 사용 (vscode에서 자동완성 지원)
  };
};

value를 상수로 생성하고, 

dispatch() 안에 있던 액션객체를 import 한 Action Creator들로 변경한다.

// src/App.js

import React from "react";
import { useDispatch, useSelector } from "react-redux";

// 사용할 Action creator를 import 한다.
import { plusOne } from "./redux/modules/counter";

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

  return (
    <div>
      {number}
      <button
        onClick={() => {
          dispatch(plusOne()); // 액션객체를 Action creator로 변경한다.
        }}
      >
        + 1
      </button>
    </div>
  );
};

export default App;

❶ dispatch() 안에는 반드시 객체만 들어가야 한다고 알았지만, 함수가 들어갈 수 있는 이유!

  • {type: “PLUS_ONE”} === plusOne()는 같은 값이다.
  • 함수를 실행한 것은 함수의 return 값과 같다.
  • 즉, const one = () => {return 1; }로 함수를 만들었을 때 one() === 1이다.

 

❷ Action creator를 사용해야 하는 이유

  • 휴먼에러 (오타) 방지
  • 유지보수의 효율성 증가 
  •  코드 가독성 높임

 

 

⑥ payload

state를 변경할 때 리듀서에게 어떤 값을 같이 보내줘야 하는 경우,

payload를 액션객체에 같이 담아 보낼 수 있다.

// src/redux/modules/counter.js

// Action Value
const ADD_NUMBER = "ADD_NUMBER";

// Action Creator
export const addNumber = (payload) => {
  return {
    type: ADD_NUMBER,
    payload: payload,
  };
};

payload가 필요한 Action Creator에서는 함수를 선언할 때 매개변수 자리에 paylaod를 넣어줘야 한다.

Action Creator를 사용하는 컴포넌트에서 리듀서로 보내고자 하는 payload를 인자로 넣어줘야 하기 때문이다.

// 리듀서

const counter = (state = initialState, action) => {
  switch (action.type) {
    case ADD_NUMBER:
      return {
// state.number (기존의 nubmer)에 action.paylaod(유저가 더하길 원하는 값)을 더한다.
        number: state.number + action.payload,
      };
    default:
      return state;
  }
};

사용자가 컴포넌트에서 Action Creator로 payload를 담아 보내는 것은 액션객체에 담기고,

그렇게 담긴 것은 리듀서에서 action.payload에서 꺼내 사용해서 기존의 값에 더해줌으로써 기능을 구현하는 것이다.