공부/React

리액트 숙련주차 - 5

뀨뿌뀨뿌 2023. 7. 11. 00:01

1.  리덕스 설정

ⅰ. 리덕스 설치

  • 리액트에서 리덕스를 사용하기 위해서는 2개의 패키지를 설채해야함
yarn add redux react-redux
  • 폴더 구조 생성하기

① src 폴더 안에 redux 폴더를 생성
② redux 폴더 안에 config, modules 폴더를 생성
③ config 폴더 안에 configStore.js 파일을 생성함
❓폴더와파일의 역활
- redux: 리덕스와 관련된 코드를 모두 모아 놓은 폴더
- config: 리덕스 설정과 관련된 파일들을 놓은 폴더
- configStore: "중앙 state 관리소"인 Store를 만드는 설정 코드들이 있는 파일
- modules: State들의 그룹, ex) ToDoList에 필요한 state들이 모여있는 todosljs 파일이 곧 하나의 모듈이 됨

ⅱ. 설정코드 작성

  • 설정 코드 작성 시 주의사항
    • 설정코드는 이해할 필요가 없는 코드이며, 코드 분석 금지!!!!!
    • 설정 코드를 작성하는 이유는 리덕스를 만든 리덕스팀에서 이렇게 설정하라고 안내하고 있기 때문
  • src/confilgStore.js
import { createStore } from "redux";
import { combineReducers } from "redux";

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

export default store;
// 원래부터 있던 코드
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();

✔️ <Provider store ={store}></Provider>로 감싸줘야함

리액트에서 리덕스를 사용하려면, redux, react-redux가 필요함
중앙 State 관리소를 Store(스토어)라고 부름
모듈이란, State들이 그룹

2. Redux란?

ⅰ. 리덕스가 필요한 이유

  • useState의 불편함
    • 어떤 컴포넌트애서 생성한 state를 다른 컴포넌트로 보고자 할 때 Props를 통해 부모 컴포넌트에서 자식 컴포넌트로 그 값을 보내줌
    • Props로 State를 공유하는 방법의 불편함
      • 컴ㅁ포넌트에서 컴포넌트로 State를 보내기 위해서는 반드시 부-모 관게가 되어야함
      • 조부모 컴포넌트에서 손자 컴포넌트로 값을 보내고자 할 때도 반드시 부모 컴포넌트를 거쳐야함
        => 부모컴포넌트에서 그 값이 필요가 없어도 단순히 손자 컴포넌트에게 전달하기 위해 불필요하게 거쳐야만 하는 것을 의미함
      • 자식 컴포넌트에서 부모 컴포넌트로 값을 보낼 수 없음

  • 이러한 불편함을 해결하기 위해서 사용하는 것이 바로 리덕스
  • 리덕스를 사용하면 State를 공유하고자 할때 부-모 관계가 아니여도 되고, 중간에 의미없는 컴포넌트를 거치지 않아도됨
  • 자식컴포넌트에서 만든 State를 부모컴포넌트에서도 사용할 수 있게 됨
  • Global state와 Local state
    • Local state(지역상태)란?
      • 컴포넌트에서 useState를 이용해서 생성한 state
      • 좁은 범위 안에서 생성된 State라고 생각하면 됨
    • Global state(전역상태)란?
      • Global state는 컴포넌트에서 생성되지 않음.
      • 중화화 된 곳에서 state들이 생성됨 -> 중앙 state 관리소라고 생각하면 됨
    • 중앙 state관리소에서 State를 생성하고, 만약 어떤 컴포넌트에서 State가 필요하다면 컴포넌트가 어디에 위치하고 있든 상관없이 State를 불러와서 사용할 수 있게 됨
    • 특정 컴포넌트에 종속되어 있는 것이 아니라 중앙 state관리소에서 생성된 State를 Global state라 하는데 이러한 값들을 관리하는 것을 전역 상태 관리라고 함

 

ⅱ. 리덕스란?

  • 리덕스 정의
    • 리덕스란 중앙 state관리소를 사용할 수 있게 도와주는 패키지(라이브러리)임
    • 프론트엔드 개발자들은 리덕스를 전역 상태관리 라이브러리 라고 많이 표현함
    • 전역상태, 즉 Global State를 의미하고 그것을 관리하게 도와주는 라이브러리(패키지)이기 때문
리덕스는 전역상태 관리 라이브러리
리덕스는 useState를 통해 상태를 관리했을 때 발생하는 불편함을 일부 해소시켜 줌
리덕스는 중앙 State 관리소를 가지고 있으며, 모든 State는 이곳에서 생성됨
useState로 생성한 State는 Local State, 리덕스에서 생성한 State는 Global State

3. 카운터 프로그램 만들어보기

ⅰ. 모듈 만들기

  • modles 폴더에 counterjs 파일을 생성
// src/modules/counter.js

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

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

// 모듈파일에서는 리듀서를 export default 한다.
export default counter;
  • 모듈 구성요소
    • initialState === 초기 상태값
      • 어떤 State의 초기값을 정해주는 것
      • useState를 사용했을 때 괄호 안에 초기값을 지정해주던 것과 같은 이치
      • 위에 코드에서 만든 State의 초기값은 객체이고 그안에 number라는 변수에 초기값을 0으로 할당
      • 초기값은 객체가 아니여도 되고 배열이나 원시데이터도 가능함
    • Reducer === 변화를 일으키는 함수
      • 리듀서란 변화를 일으키는 함수
      • useState()를 사용할 때, number라는 값을 바꾸고 싶으면 setNumber을 사용해야 했음
      • 하지만 리덕스에서는 리듀서가 이 역할을 해줌
      • 리듀서는 인자 첫번째 자리에서는 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의 훅을 사용해야함
  • useSelect 사용 방법
// 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
});

// 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;
  • 브라우저의 콘솔에서 보면 객체 안에 counter라는 값이 있음
  • counter라는 모듈의 state가 보이는 것을 알수 있게 됨
  • 화살표함수에서 꺼낸 state라는 인자는 프로젝트에 존재하는 모든 리덕스 모듈의 state
  • 컴포넌트에서 number라는 값을 사용하고자 한다면 아래 코드처럼 사용하면됨
const number = useSelector(state => state.counter.number); // 0

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

  • 리덕스에 내르는 명령을 Action이라고 함
  • 리듀서에게 내가 어떤 Atcion을 하길 원하는것을 표현하는 행동을 코드로 나타내면 객체를 만드는데 이것을 액션 객체라고 함
  • 액션 객체는 반드시 type이라는 key를 가져야함
    ❓ 이 액션 객체를 리듀서에게 보냈을 때 리듀서는 객체 안에서 type이라는 key를 보기 때문
// 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;

// 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;
액션객체란, 반드시 type이랑 key를 가져야 하는 객체이며 리듀서로 보낼 명령
디스패치란, 액션객체를 리듀서로 보내는 전달자 함수
리듀서란, 디스패치를 통해 전달받은 액션객체를 검사하고, 조건이 일치했을 때 새로운 상태값을 만들어내는 변화를 만들어내는 함수
디스패치(dispatch)를 사용하기위해서는 useDispatch()라는 훅을 이용해야 함
  - 디스패치는 스토어의 내장함수 중 하나
  - 디스패치는 액션을 발생 시키는 것 정돋로 이해하면 됨
  - dispatch라는 함수에는 액션을 파라미터로 전달함 => dispatch(action)이런식으로
액션객체 type의 value는 대문자로 작성해야함(JS에서 상수는 대문자로 작성하는 룰이 있음)

3.리덕스의 흐름 도식화

출처 : https://velog.io/@annahyr/리덕스-흐름-이해하기

  1.  View에서 액션이 일어남
  2. dispatch에서 action이 일어나게 됨
  3. action에 의한 reducer 함수가 실행되기 전에 middlewarerk wkrehdgka
  4. middleware에서 명령내린 일을 수행하고 난 뒤, reducer 함수 실행
  5. reducer의 샐행결과 store에 새로운 값을 저장함
  6. store의 state에 subscribe하고 있던 UI에 변경된 값을 줌

 

4. Redux-Refactoring(action creators, action values)

ⅰ. Action Creator

  • Action Create - 액션을 만드는 생성자
  • 액션객체의 value를 변경할 일이 생겼는데 프로젝트 규모가 커서 변경해야 할 곳이 여러군데면  문제가 생길 수 있음
// 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" }); // counter/PLUS_ONE로 변경
        }}
      >
        + 1
      </button>
      <button
        onClick={() => {
					// 액션객체 디스패치
          dispatch({ type: "MINUS_ONE" }); // counter/MINUS_ONE로 변경
        }}
      >
        - 1
      </button>
    </div>
  );
};

export default App;

// src/modules/counter.js

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

// 리듀서
const counter = (state = initialState, action) => {
  switch (action.type) {
    case "PLUS_ONE": // counter/PLUS_ONE로 변경
      return {
        number: state.number + 1,
      };

		// action.type이 MINUS_ONE 일 때 새로운 state 반환
    case "MINUS_ONE": // counter/MINUS_ONE로 변경
      return {
        number: state.number - 1,
      };
    default:
      return state;
  }
};

// 모듈파일에서는 리듀서를 export default 한다.
export default counter;
  • Action Creator 만들기
    • 위에 코드처럼 하드코드가 아닌 액션객체를 한곳에서 관리 할 수 있도록 함수와 액션 value를 상수로 만듦
    • 만약에 PLUS_ONE 이라는 액션 객체를 만드는 함수를 만들면 아래와 같이 만들 수 있음
// src/redux/modules/counter.js

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

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

✔️ 액션이 value는 상수로 따로 만둘고, 그것을 이용해서 액션 객체를 반환하는 함수를 작성

  • 리듀서 코드에 적용하기
// src/modules/counter.js

// 추가된 코드 👇 - 액션 value를 상수들로 만들어 줍니다. 보통 이렇게 한곳에 모여있습니다.
const PLUS_ONE = "PLUS_ONE";
const MINUS_ONE = "MINUS_ONE";


// 추가된 코드 👇 - Action Creator를 만들어 줍니다. 
export const plusOne = () => {
  return {
    type: PLUS_ONE,
  };
};

export const minusOne = () => {
  return {
    type: MINUS_ONE,
  };
};


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

// 리듀서
const counter = (state = initialState, action) => {
  switch (action.type) {
    case PLUS_ONE: // case에서도 문자열이 아닌, 위에서 선언한 상수를 넣어줍니다. 
      return {
        number: state.number + 1,
      };
    case MINUS_ONE: // case에서도 문자열이 아닌, 위에서 선언한 상수를 넣어줍니다. 
      return {
        number: state.number - 1,
      };
    default:
      return state;
  }
};


export default counter;
  • Action Creator 사용하기
    • 사용 순서
      • export 된 Action Creator를 import 하기
      • dispatch()에 있던 액션객체를 지우고 Action creator 넣기
// src/App.js

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

// 사용할 Action creator를 import 합니다.
import { minusOne, 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>
      {/* 빼기 버튼 추가 */}
      <button
        onClick={() => {
          dispatch(minusOne()); // 액션객체를 Action creator로 변경합니다.
        }}
      >
        - 1
      </button>
    </div>
  );
};

export default App;
Q. dispatch()안에는 반드시 객체만 들어가야하는데 함수가 들어갈 수 있는 이유는?
{type: "PLUS_ONE"} === plusOne()은 같은 값
함수를 실행한 것은 함수의 return 값과 같음 => const one () => {return 1;} 로 함수를 만들었을 때 one() === 1

ⅱ. Action creator를 사용하는 이유

  • 휴먼에러 (오타) 방지
    • 액션 객체의 type value를 상수로 만들어놓았기 때문에 개발툴에서 자동완성등의 보조 기능을 지원받을 수 있음
    • 따라서 의도치 않은 휴먼에러 없앨수 있음
  • 유지 보수의 효율성 증가
    • 여러군데에서 쓰이고 있는 상태에서 그것을 바꿔야 하는 상황이 있더라도 수정 한번으로 수정사항을 모든데 반영할 수 있음
  • 코드 가독성
    • 모듈 파일에서 Action Creator가 정리가 되어있으면 다른 사람이 보았을 때 해당 모듈이 가지고 있는 모든 Action들을 한눈에 알 수 있게 됨
    • 그 자체가 Action들의 리스트업을 해주는 역할을 갖게 되는 것
  • 리덕스 공식문서에서 소개되고 있는 방법
액션객체를 만드는 함수를 Action Creator라고 함
Action Creator는 모듈 파일안에서 생성
액션 객체의 type value로 상수로 생성해서 관리함
Action Creator를 사용하면 여러가지 문제점을 해결할 수 있음

5. Redux - Payload 및 Ducks 패턴

ⅰ. Payload란?

  • 목적어가 생긴 액션 객체를 같이 담아서 보내주는 것을 payload라 함
  • State를 변경하는데 있어 리듀서에게 어떤 값을 보내줘야 한다면 payload를 액션객체에 같이 담아 보냄

❓payload라는 이름을 통해서 보내야 보내야 하는지?
✔️ 리덕스는 굉장히 유연한 라이브러리이기 때문에 많은 것들이 표준화 되어 있지 않기 때문에 자신만의 방식으로 프로그래밍 할 수 있는 유연성을 제공함
✔️ 리덕스 공식 문서를 확인해보면 액션은 객체이며 해당 액션이 어떤 기능을 수행해야 하는지 명시하는 type이라는 프로퍼티를 반디시 가져야 한다고 나와있음 하지만 그 외에 데이터들을 어떤 프로 퍼티에 값으로 넣어줘야 하는지는 개발자 마음

{type: "ADD_NUMBER", num: 10} // ??
{type: "ADD_NUMBER", number: 10} // ??
{type: "ADD_NUMBER", data: 10} // ??
{type: "ADD_NUMBER", myNumber: 10} // ??
{type: "ADD_NUMBER", myNum: 10} // ??


{type: "ADD_NUMBER", payload: 10}

✔️ 위에 작성한 코드는 모두 요효한 코드이지만 데이터를 paylod프로퍼티에 담아주는 이유는 커뮤니티에서 "커뮤니티 best practice"로 공유되면서 많은 개발자가 데이터는 payload라는 프로퍼티에 담아주고 있음
커뮤니티 컨벤션이기 때문에 개발하는 과정에선 개발자 취향과 상황에 따라 변경할 수 있음

ⅱ. payload 이용해서 기능 구현하기

  • payload를 이용해서 기능 구현 작업 순서
    • 사용자가 입력한 값을 받을 input 구현하기
    • Action Creator 작성하기
    • 리듀서 작성하기
    • 구현된 기능 테스트하기
  • 사용자가 입력받을 input값 구현하기
// src/App.js


import React from "react";
import { useState } from "react";

const App = () => {
  const [number, setNumber] = useState(0);

  const onChangeHandler = (event) => {
    const { value } = event.target;
		// event.target.value는 문자열 입니다.
		// 이것을 숫자형으로 형변환해주기 위해서 +를 붙여 주었습니다.
    setNumber(+value);
  };

	// 콘솔로 onChangeHandler가 잘 연결되었는지 확인해봅니다.
	// input에 값을 넣을 때마다 콘솔에 그 값이 찍히면 연결 성공!
  console.log(number);

  return (
    <div>
      <input type="number" onChange={onChangeHandler} />
      <button>더하기</button>
      <button>빼기</button>
    </div>
  );
};

export default App;
  • counter.js 모듈 작성 - Action Creator
    • 작성해야 하는 것들
      • Action Value
      • Action Creator
      • Initial Sate
      • Reducer
      • export default reducer
// src/redux/modules/counter.js

// Action Value
const ADD_NUMBER = "ADD_NUMBER";

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

// Initial State

// Reducer

// export default reducer
  • Initial State, Reducer, 내보내기(export default) 추가히기
// src/redux/modules/counter.js

// .. 중략

// Initial State
const initialState = {
  number: 0,
};

// Reducer 기본형태
const counter = (state = initialState, action) => {
  switch (action.type) {
    default:
      return state;
  }
};

// 리듀서

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

// export default reducer
export default counter;
  • 구현된 기능 테스트 하기
import React from "react";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";

// 4. Action Creator를 import 합니다.
import { addNumber } from "./redux/modules/counter";

const App = () => {
	// 1. dispatch를 사용하기 위해 선언해줍니다.
  const dispatch = useDispatch();
  const [number, setNumber] = useState(0);
  const globalNumber = useSelector((state) => state.counter.number);

  const onChangeHandler = (event) => {
    const { value } = event.target;
    setNumber(+value);
  };

	// 2. 더하기 버튼을 눌렀을 때 실행할 이벤트핸들러를 만들어줍니다.
  const onClickAddNumberHandler = () => {
		// 5. Action creator를 dispatch 해주고, 그때 Action creator의 인자에 number를 넣어줍니다.
    dispatch(addNumber(number));
  };

  return (
    <div>
      <div>{globalNumber}</div>
      <input type="number" onChange={onChangeHandler} />
			{/* 3. 더하기 버튼 이벤트핸들러를 연결해줍니다. */}
      <button onClick={onClickAddNumberHandler}>더하기</button>
      <button>빼기</button>
    </div>
  );
};

export default App;

ⅲ. Ducks 패턴

  • Ducks 패턴이란?
    • 리덕스 모듈을 개발하는 개발자마다 구성요소들을 제각각 구현하면 협업을 해야 하는 상황에 놓였을 때 수많은 파일 중에 필요로 하는 구성요소를 찾는것이 어려워짐
    • Erik Rasmussn 라는 개발자가 이것을 패턴화하여 작성하는 것을 제안햇는데 ㅇ것이 바로 Ducks 패턴
 

GitHub - erikras/ducks-modular-redux: A proposal for bundling reducers, action types and actions when using Redux

A proposal for bundling reducers, action types and actions when using Redux - GitHub - erikras/ducks-modular-redux: A proposal for bundling reducers, action types and actions when using Redux

github.com

  • Duck 패턴으로 작성하기
    • Reducer 함수를 export default 함
    • Action creator 함수들을 export 함
    • Action type은 app/reducer/ACTION_TYPE 형태로 작성(외부 라이브로리로서 사용될 경우 나 외부 라이브러리가 필요할 경우에는 UPPER_SNAKE_CASE로만 작성해도 됨)
리듀서로 보내는 액션객체에 어떤 정보를 같이 담아 보내고자 한다면 payload를 이용
payload는 Action Creator를 생성할 때 매개변수에 자리에서 받을 준비를 하고 반환하는 액션객체에 payload라는 Key와 받은 매개변수를 value로하여 구현함
리듀서에서 payload를 사용하고자 할 때는 action.payload로 사용할 수 있음
ES6에서 객체를 생성할 대 key와 value가 같으면 축약 가능
Ducks 패터는 Erik Rasmussen 이 제안했고, 현재 리덕스 모듈 작성방법의 정석으로 여겨짐

'공부 > React' 카테고리의 다른 글

리액트 숙련주차 - 4  (0) 2023.07.10
리액트 숙련주차 - 3  (0) 2023.07.05
리액트 숙련주차 - 2  (0) 2023.06.30
리액트 숙련주차 - 1  (0) 2023.06.30
리액트 입문주차 1주차 - 5  (0) 2023.06.29