[TIL] 내일배움캠프 React 과정 2022.12.11_Redux

Today I Learned 

  • React 숙련 강의 수강

 


React 숙련 강의

리덕스의 흐름 도식화

  1. View 에서 액션이 일어난다.
  2. dispatch 에서 action이 일어나게 된다.
  3. action에 의한 reducer 함수가 실행되기 전에 middleware가 작동한다.
  4. middleware 에서 명령 내린 일을 수행하고 난 뒤, reducer 함수를 실행한다. (3, 4번은 아직 몰라도 됩니다!)
  5. reducer 의 실행결과 store에 새로운 값을 저장한다.
  6. store의 state에 subscribe 하고 있던 UI에 변경된 값을 준다.

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

 

 

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

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

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

 

 리듀서에게 보낼 “명령” 만들기 

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

리덕스에서는 리듀서에 보낼 명령을 Action 이라고 하는데 이것이 객체 형태로 되어 있기 때문에 액션 객체라고 부른다. 액션 객체는 반드시 type이라는 key를 가져야한다. 왜냐하면 액션 객체를 리듀서에게 보냈을 때 리듀서는 객체 안에서 type이라는 key를 보기 때문이다. 또한 액션 객체 type의 value는 대문자로 작성한다. (JS에서 상수는 대문자로 작성하는 룰이 있음)

 

 액션 객체(명령) 보내기 - useDispatch 

// src/App.js
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;

액션 객체를 리듀서로 보내기 위해서는 useDispatch라는 훅을 사용해야 한다. useDispatch라는 훅을 사용하기 위해서는 컴포넌트 안에서 아래와 같이 먼저 코드를 작성해서 dispatch라는 변수를 생성해줘야 한다. 이렇게 생성한 dispatch는 함수 이기 때문에 dispatch를 사용할 때 () 를 붙여서 함수를 실행하게 된다. dispatch(action) 함수를 실행하면 액션을 파라미터로 전달한다.

 

 액션 객체 받기 

// src/redux/modules/counter.js

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

// 리듀서
const counter = (state = initialState, action) => {
  // console.log(action)로 action을 잘 받고 있는지 확인하기
  console.log(action); 
  switch (action.type) {
    default:
      return state;
  }
};

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

App.js에서 dispatch를 통해서 보낸 액션 객체가 리듀서로 잘 들어가고 있음을 확인할 수 있다.

 

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

// src/modules/counter.js

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

// 리듀서
const counter = (state = initialState, action) => {
  console.log(action);
  switch (action.type) {
    // PLUS_ONE이라는 case를 추가한다.
    case "PLUS_ONE":
      return {
	// 기존 state에 있던 number에 +1을 더한다.
        number: state.number + 1,
      };

    default:
      return state;
  }
};

export default counter;
  1. 컴포넌트로부터 dispatch를 통해 액션 객체를 전달받는다.
  2. action 안에 있는 type을 스위치문을 통해 하나씩 검사해서, 일치하는 case를 찾는다.
  3. type과 case가 일치하는 경우에, 해당 코드가 실행되고 새로운 state를 반환(return) 한다.
  4. 리듀서가 새로운 state를 반환하면, 그게 새로운 모듈의 state가 된다.

 useSelector로 변경된 state값 확인하기 

// src/App.js
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;

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

 

정리하기

  • 액션 객체란, 반드시 type이란 key를 가져야 하는 객체이다. 또한 리듀서로 보낼 “명령"이다.
  • 디스패치란, 액션 객체를 리듀서로 보내는 “전달자” 함수이다.
  • 리듀서란, 디스패치를 통해 전달받은 액션객체를 검사하고, 조건이 일치했을 때 새로운 상태 값을 만들어내는 "변화를 만들어내 함수"이다.

 

Action Creator란?

  • 액션 객체를 만드는 함수를 Action Creator(액션 크리에이터)라고 한다.
  • Action Creator는 모듈 파일 안에서 생성된다.
  • 액션 객체의 type value를 상수로 생성해서 관리한다.
  • Action Creator를 사용하면, 여러 가지 문제점을 해소할 수 있다.

 

Action creator를 사용하는 이유

1. 휴먼에러(오타) 방지

액션 객체의 type value를 상수로 만들어 놓았기 때문에, 개발 툴에서 자동 완성 등의 보조 기능을 지원받을 수 있다. 그래서 의도치 않은 휴먼에러(오타)를 없앨 수 있다.

2. 유지 보수의 효율성 증가

액션 객체의 value를 바꾸어야 하는 상황이 오더라도 단 한 번의 수정으로 모든 수정 사항을 반영할 수 있다.

3. 코드 가독성

모듈 파일에서 Action Creator가 일목요연하게 정리가 되어있으면, 다른 개발자가 보았을 때에도 해당 모듈이 가지고 있는 모든 Action들을 한눈에 알 수 있게 된다. 즉 그 자체가 Action 들의 리스트업을 해주는 역할이 되는 것이다.

 

 

Action Creator로 액션 객체 만들기

 Action Creator 만들기 

// src/redux/modules/counter.js

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

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

액션 객체를 한 곳에서 관리할 수 있도록 “함수"와 액션 value를 상수로 만들어야 한다. 액션의 value는 상수로 따로 만들고, 그것을 이용해서 액션 객체를 반환하는 함수를 작성하는데 이것이 Action Creator인 것이다. 

 

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

모듈에 기존의 initialState와 리듀서 외에 액션의 value와 Action Creator가 추가되었다.

 

 Action Creator를 컴포넌트에서 사용하기 

// src/App.js
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>
  );
};
  1. export된 Action Creator import 하기
  2. dispatch()에 있던 액션 객체를 지우고, Action creator 넣기

 

Payload란?

// payload가 추가된 액션객체
{type: "ADD_NUMBER", payload: 10} // type뿐만 아니라 payload라는 key와 value를 같이 담는다.

만약에 사용자가 5를 더하고 싶으면 어떤 input에 5를 입력해서 버튼을 누르면 5가 더해지고, 10을 더하고 싶으면 10을 입력하고 버튼을 눌렀을 때 10이 더해지는 프로그램을 만들고 싶을 때 액션 객체에 payload를 추가해서 사용할 수 있다. 이렇게 액션 객체에 어떤 값을 같이 담아 보내는 것을 payload라고 한다.

 

 

payload를 이용해서 기능 구현하기

payload 이용해서 기능 구현 작업 순서

  1. 사용자가 입력한 값을 받을 input 구현하기
  2. Action Creator 작성하기
  3. 리듀서 작성하기
  4. 구현된 기능 테스트 하기

 사용자가 입력한 값을 받을 input 구현하기 

// src/App.js
import { useState } from "react";

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

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

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

input의 값을 state로 관리하기 위해 훅을 사용하여 useState를 작성하고, 이벤트 핸들러 (onChangeHandler)를 작성하여 input과 연결한다.

 

 counter.js 모듈 작성 

// Action Value

// Action Creator

// Initial State

// 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: payload,
  };
};

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

// Reducer
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;

payload가 필요한 Action Creator에서는 함수를 선언할 때 매개변수 자리에 paylaod를 넣어야 한다. Action Creator를 사용하는 컴포넌트에서 리듀서로 보내고자 하는 payload를 인자로 넣어줘야 하기 때문이다.

 

사용자가 컴포넌트에서 Action Creator로 payload를 보내는 것은 액션 객체에 담기고, 그렇게 담긴 것은 리듀서의 action.payload에서 꺼내 사용할 수 있다. 이것을 이용해서 기존의 값에 더해줌으로써 기능을 구현하는 것이다.

 

 구현된 기능 테스트 하기 

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

우선 App.js에서 useSelector를 이용해서 Store의 값을 조회하고 그것을 화면상에 렌더링하는 기능을 추가한다. 그리고 Action Creator를 import 하고, payload를 담아 dispatch한다. 

 

 

Ducks 패턴이란?

Erik Rasmussen 이 제안한 Ducks 패턴은 아래의 내용을 지켜 모듈을 작성하는 것으로 모듈 파일 1개에 Action Type, Action Creator, Reducer 가 모두 존재하는 방식이다. 

  1. Reducer 함수를 export default 한다.
  2. Action creator 함수들을 export 한다.
  3. Action type은 app/reducer/ACTION_TYPE 형태로 작성한다.

 

payload 정리하기

  • 리듀서로 보내는 액션 객체에 어떤 정보를 같이 담아 보내고자 한다면 payload를 이용한다.
  • payload는 Action Creator를 생성할 때 매개변수 자리에서 받을 준비를 하고, 반환하는 액션 객체에 payload라는 key와 받은 매개변수를 value로 하여 구현한다.
  • 리듀서에서 payload를 사용하고자 할 때는 action.payload로 사용할 수 있다.

 


Programmers 문제 풀기

영어가 싫어요

function solution(numbers) {
    let array = []
    if (numbers.includes('one')) {
         let one = numbers.replace('one', '1')
         array.push(one)
    }
    if (numbers.includes('two')) {
        let two = numbers.replace('two', '2')
        array.push(two)
    }
    // two = numbers.replace('two', '2')
    // three = numbers.replace('three', '3')
    // four = numbers.replace('four', '4')
    // five = numbers.replace('five', '5')
    // six = numbers.replace('six', '6')
    // seven = numbers.replace('seven', '7')
    // eight = numbers.replace('eight', '8')
    // nine = numbers.replace('nine', '9')
    // zero = numbers.replace('zero', '0')
    return array;
    
    // return answer
}

 

며칠 째 해결하지 못하고 있는 문제 ㅋㅋ

 

function solution(numbers) {
    
    let answer = ''
    let answer2 = ''
    
    answer += numbers.replace('one', '1')
    answer += numbers.replace('two', '2')
    answer += numbers.replace('three', '3')
    answer += numbers.replace('four', '4')
    answer += numbers.replace('five', '5')
    answer += numbers.replace('six', '6')
    answer += numbers.replace('seven', '7')
    answer += numbers.replace('eight', '8')
    answer += numbers.replace('nine', '9')
    answer += numbers.replace('zero', '0')
    
    for (const num of answer) {
        if (!isNaN(num)) {
            answer2 += num
        }
    }
    
    return answer2
}

일단 코드를 수정해서 어느 정도 정답과 비슷하게 출력할 수 있었다. replace()로 문자열에 있는 문자를 숫자로 변환한 후에 각 문자열을 모두 이어 붙인다.(answer의 출력값) 문자열과 숫자가 어지럽게 섞여있는 answer에서 숫자만 골라서 빈 문자열인 answer2에 넣어 준다. 그러면 일단 숫자만 골라서 결과를 출력할 수 있다.

 

그런데 아직 문제가 남아 있는데 내가 출력한 결과는 숫자가 1부터 차례대로 정렬된다. 그래서 기댓값과 다른 결괏값이 출력된다. 또한 만약에 문자열에 동일한 숫자가 있을 경우에 이것도 출력하지 못한다. 그리고 지금 출력한 값 보니까 숫자가  문자열 형태로 출력되고 있는 거 같다.

 

문제 풀 때 참고한 링크

https://jieum.tistory.com/30

 


회고

역시 주말이라 집중이 잘 안 된다. 원래 목표는 주말 동안 리액트 숙련 강의 다 듣는 거였는데 결국 2강을 남겨두고 주말을 마무리한다. 하루 종일 리덕스 공부만 하는데 비슷한 용어가 많아서 너무 헷갈린다. 액션 객체, action value, action creator 등등 아직 명확하게 개념이나 관계가 잡히지 않는다. state 전역으로 쓰겠다고 추가적으로 작성해야 하는 코드가 너무 많은 거 같다.😥 거기다가 파일도 막 왔다 갔다 하니까 뭐를 어디에 작성해야 하는지도 너무 어렵고.. 그래도 일단 숙련 과제하면서 직접 작성해보면 구조를 더 잘 이해할 수 있을 거라 믿는다. 제발..!