쇼핑몰 프로젝트(redux/reducer)

코딩애플 님의 강의를 바탕으로한 글입니다:)

장바구니 기능

장바구니 기능을 만들어보려 한다. 따라서, Cart.js 안에 리엑트부트스트랩에서 가져온 표를 넣어보자. 이때 export, import 하는 것 잊지말고, Route 해오는 것도 잊지말자.

<Cart.js>
상단
import React from "react";
function Cart( 
	내용
)
하단
export default Cart
<App.js>
상단
import Cart from "./Cart";
function App( 
 	.
    .
	<Route path:/cart>
    <Cart></Cart>
    </Route>
    .
    .
)

저번 시간에 상위컴포넌트의 state(데이터)를 받아올 때 두 가지 방법을 배웠다.
끝없는 props지옥을 만들거나, ContextAPI를 통해 원하는 데이터를 가진 컴포넌트를 createContext()로 감싸서, 값을 사용하였다.

이번에는 redux라는 라이브러리를 가지고 props과정을 쉽게 단축시켜보자.

redux

앞서 만든 Cart라는 페이지에도 상품에 대한 데이터가 필요할 것이다. 그럼 데이터는 어디에 저장할까? 역시 이제 기본적으로 상위컴포넌트에 저장하는 것을 알고 있다. 만약 대형프로젝트라면 수많은 컴포넌트가 존재하고, Cart페이지까지 데이터를 받아올때까지 또 props지옥은 시작될 것이다.
따라서, redux는 모든 컴포넌트파일들이 같은 값을 공유할 수 있는 저장공간을 생성하는 것이 가능하게 하고 state 데이터를 관리기능한다.

  1. 설치
yarn add redux react-redux 또는 npm install redux react-redux
  1. 최상위 컴포넌트라고 할 수 있는 index.js에서 Provider 라는 것을 react-redux에서 import 해온다.
<index.js>
import {Provider} from 'react-redux';
  1. state값을 공유하기를 원하는 컴포넌트를 감싼다.
    이를 통해 App컴포넌트와 그 안의 HTML,컴포넌트들은 모두 state값을 사용 가능
<index.js>
import {Provider} from 'react-redux';
ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <Provider>
        <App/>
      </Provider>
    </BrowserRouter>
  </React.StrictMode>
);
  1. redux에서 state를 하나 만들기위해 createStore() 함수를 사용
    그러면 createStore 속 콜백함수가 state 저정값을 만든다.
    즉, useState로 상품 저장하는 것과 비슷한 개념이다.
    redux를 설치 후 사용하는 state들은 store이라고 명칭.
(index.js)
import {Provider} from 'react-redux';
import {createStore} from 'redux';
let store = createStore(()=>{ return [{id : 0, name : '멋진신발', quan : 2}]  })
  1. Provider에 store값을 props처럼 등록을 해놓는다.
<Provider store={store}>
        <App/>
      </Provider>
  1. 이제 store에 저장된 데이터들을 받아와 쓰면 된다. 앞서 만든 Cart.js에서 받아와 보자.
  • 우선 connect라는 컴포넌트(함수)를 react-redux에서 import해온다.
  • connect함수는 컴포넌트들을 store에 연결시켜주는 역할.(state는 store에 저장되어 있으니까)
  • mapStateToProps의 첫번째 인자인 state를 자유작명한 state(자유작명이름)에 등록.
    state: state는 전체 state에 대한 데이터를, state: state.name은 원하는 state만 등록이 가능
(Cart.js)
import 많은곳;
import {connect} from 'react-redux';
function Cart(){
  return (
    상품 표 내용
  )
}
function mapStateToProps(state){
  return {
    state : state
  }
}
export default connect(mapStateToProps)(Cart);
  1. 이제 데이터바인딩을 자유롭게 할 수 있는데, props를 작성후 가져다 쓰면 되겠다.
(Cart.js)
function Cart(props){
  return (
    <div>
          <td>1</td>
          <td>{props.state[0].name}</td>
          <td>Table cell</td>
          <td>Table cell</td>
        </tr>
      </Table>
    </div>
  )
}```

connect 함수의 기본 구문

connect(mapStateToProps, mapDispatchToProps)(Home);

  • mapStateToProps(첫번째 인자,함수): store로부터 state를 가져와서 컴포넌트의 props로 보내게 해준다.
  • mapDispatchToProps(두번째 인자): dispatch를 props로 보낼 수 있다.
  • Home: 취득한 데이터를 props로 사용하고 싶은 컴포넌트를 지정한다.

간단한 설명)
export default connect(mapStateToProps)(Cart)
= Cart컴포넌트에 store로 부터 가져온 state를 props로 등록해주는(바꿔주는) 연결(connect)

참고 및 자세한 설명) https://velog.io/@iamhayoung/React-Redux-React-Redux-%EC%9E%85%EB%AC%B8-Provider-Connect-mapStateToProps-mapDispatchToProps#3%EF%B8%8F%E2%83%A3-mapstatetoprops

대략적인 정리

일반적인 props 방법
상위컴포넌트 속 state -> props-> props -> ... -> props -> 하위컴포넌트 속 props.state명

ContextAPI
React.createContext()생성 -> 상위컴포넌트 -> 하위컴포넌트 속 useContext()로 사용

redux
상위컴포넌트 -> createStore()로 store생성 -> connect()을 통해 데이터 받아오기

그렇다면 redux 와 contextAPI는 큰 차이가 있을까?
둘 다 전역 상태 관리를 하지만, redux가 다양한 기능을 제공한다.
더 자세한 내용은 잘 설명해주시는 분들의 내용을 참조하자~
https://velog.io/@cada/React-Redux-vs-Context-API
https://egg-programmer.tistory.com/281

reducer로 데이터 관리하기

reducer는 이전상태의 state와 액션을 합쳐, 새로운 state로 만드는 조작을 말한다.


참고: carrot useless

만약 + 버튼을 누르면 수량이 증가하는 UI를 만들고 싶다. 그렇다면 버튼을 누르면 store에 저장된 quan의 숫자가 증가하면 된다.
이때, redux을 사용할때는 reducer함수에 수정방법에 대한 액션을 정의해놓고, 그 액션을 불러오면 된다.
핵심) redux에선 state 데이터의 수정방법을 미리 정의해야 한다.

  1. 우선, 초기(기본)값state 변수에 데이터를 저장한다.
let 기본state = [{id : 0, name : '멋진신발', quan : 2}];
  1. 그리고, reducer의 default(기본)파라미터에 저장한다.
function reducer(state = 기본state, 액션){
	...
 	return state
}
  1. 이제 reducer함수 안에 액션명과 액션에 대한 것이 정의된다.
    아래의 코드를 해석하면, '수량증가'라는 액션이 정의되었고, 이 액션이 수량을 증가시켜주는 내용이다. 즉 ,기본state이 저장된 state 파라미터를 깊은 복사한뒤, 그 중에 첫번째데이터의 quan을 1씩 증가시키는 액션이 만들어진 것이다.
    그리고 이 액션이 요청되지 않을 경우에는 else문이 실행되어 원래의 기본 state값이 실행된다.
function reducer(state = 기본state, 액션){
 if (액션.type === '수량증가') {
    let copy = [...state];
    copy[0].quan++;
    return copy
  } else {
    return state
  }
}
  1. 그리고 reducer함수를 store값에 저장하면 된다. 그러면 다른페이지의 connect()에 의해서 데이터가 전달될 것이다.(위에서 학습한 내용이 실행)
(index.js)
let 기본state = [{id : 0, name : '멋진신발', quan : 2}];
function reducer(state = 기본state, 액션){
  if (액션.type === '수량증가') {
    let copy = [...state];
    copy[0].quan++;
    return copy
  } else {
    return state
  }
}
let store = createStore(reducer);
  1. 마지막으로 '수량증가'라는 액션을 요청하기 위한 코드이다.(dispatch: 보내다)
(Cart.js에 있던 버튼)
<button onClick={()=>{ props.dispatch({type: '수량증가'}) }}> + </button>

정리)
다시말하면, redux는 수정방법을 미리 정의하기 때문에 state에러를 추적하기가 용이하다. 만약 그 수정방법에 의해 수정이 되었는데 에러가 난다면 redux의 reducer함수 내부만 확인하면 되니까. 하지만, 일반 props처럼 다른 컴포넌트들에서 state에서 가져다 쓰며 값을 변경하다가 state값에 에러가 나면 어디 컴포넌트에서 변경하다가 에러가 난 것인지 추적이 어려울 것이다.

redux 전체적인 흐름도

여러개의 state와 reducer

만약 다른 데이터가 필요하고, 그 데이터를 수정해야 한다면?
결론은 똑같이 초기값 데이터를 만들고, reducer를 또 만들어주면 된다.

Cart페이지에 alert창을 하나 만들고, 버튼 클릭시 닫히는 기능을 만들어보자.

  1. 초기값을 만들고, reducer2라는 함수를 만든다.
let alert초기값 = true;
function reducer2(state = alert초기값, action){
  if(action.type === "닫기"){
    state= false;
    return state;
  }
  else if(action.type === "열기"){
    state= true;
    return state;
  }
  else{
    return state
  }
}
}

잠깐) switch문으로 작성시 조금더 정리된 코드 작성 가능

function reducer(state, 액션){
  switch (액션.type) {
    case '수량증가' :
      return 수량증가된state;
    case '수량감소' : 
      return 수량감소된state;
    default : 
      return state
  }
}
  1. 그리고, store에 생성할때는 combineReducers() 안에 객체값으로 넣어준다.
    함수이름만 봐도 어떤 역할을 하는지 알 수 있을 것이다.
let store = createStore( combineReducers({reducer, reducer2}) )
  1. 그럼 똑같이 store을 받아올때도 약간의 변화를 주어야 한다.
    작명을 하나 더 해서, store(state)에 저장된 값중 reducer2의 값을 저장해주면 된다.
function mapStateToProps(state) {
  return {
    state: state.reducer,
    alert: state.reducer2
  };
}
export default connect(mapStateToProps)(Cart);
  1. 항상 하듯이 삼항조건자를 통해 true값일 때 출력되는 값, false일때 출력되는 값을 설정한다. (props 잊지 말자. 그리고 connect()함수로 store값이 props로 변환시켜줬다는 것을...)
 { props.alert === true
            ?
        <div className="my-alert">
            <p>지금구매하시면 신규할인 20%</p>
            <button onClick={()=>{props.dispatch({ type: "닫기"});}}>닫기</button>
        </div>
        : 
        <div>
        <button onClick={()=>{props.dispatch({ type: "열기"});}}>공지 열기</button>
        </div>
        }

잠깐)
위와 같이 alert창의 기능을 구현할 수 있다. 하지만, 이 alert기능은 Cart컴포넌트 안에서만 필요한 친구라면? 굳이 redux를 사용할 필요가 없다. 다시 말하자면 redux는 props지옥을 편하게 만드는 것이다. 즉, 어느 컴포넌트든 편리하게 받아오려고 쓰는 라이브러리인데, alert기능은 단 하나의 컴포넌트에서만 사용한다. 그렇다면 useSate()을 사용하여 값을 저장하여 사용하는 것이 더 좋은 선택이다.

dispatch를 통해 데이터 보내기

앞서 학습을 할때, 데이터의 수정을 원할때 액션 수행 명령을 주었는데, 아래의 코드 같이 dispatch() 안에 객체들을 주었다.

<button onClick={()=>{ props.dispatch( {type : '항목추가'} ) }}>주문하기</button>

원하는 데이터를 버튼 클릭시 장바구니 리스트에 추가하는 기능을 만들어보자.
원리는 간단하다. 다음과 같이 데이터도 객체값으로 같이 실어보낼 수 있다.

      props.dispatch({type : '항목추가', payload : {id : 2, name : '새로운상품', quan : 1} }) 

dispatch 로 보내진 객체값들은 reducer의 두번째 인자인 액션에 값이 저장이 된다. 앞서 액션의 명령이라했던 props.dispatch( {type : '항목추가'} 또한 다시 말하자면, 타입이 '항목추가'인 객체를 보낸 것이고, 그 값이 액션 파라미터에 저장된 것이다. 그리고 if문의 조건에 의해 액션에 저장된 type값이 '항목추가'라면 조건문 속 코드를 실행하자는 말인 것이다. 이 과정을 액션을 수행하는 명령이라고 대략적으로 생각한 것이다.

다시 돌아와, payload에 저장된 값 또한, 깊은복사가된 copy 변수에 push()를 통해 저장된다.

function reducer(state = 기본state, 액션){

  if (액션.type === '항목추가') {

    let copy = [...state];
    copy.push(액션.payload);
    return copy;
  } 

마지막으로, 클릭 시 항목추가 명령에 따라 상품이 추가되어야 하는데, 안되는 이유는 페이지가 이동되면서 초기화가 되기 때문이다. 따라서, history.push()을 통해 기록을 가지고 가므로 초기화되지 않고 장바구니 리스트에 추가하고자 하는 상품의 정보가 출력된다.

 <button onClick={()=>{ 
      props.dispatch({type : '항목추가', payload : {id : 2, name : '새로운상품', quan : 1} });
      history.push('/cart'); 
    }}>주문하기</button>

useSelector, useDispatch

앞서 배운 것 중에 store값을 props로 바꿔주는 기능이 있었다.

function mapStateToProps(state) {
 return {
   state: state.reducer,
   alert: state.reducer2
 };
}
export default connect(mapStateToProps)(Cart);

이 방법말고 다른 방법을 알아보자. (위의 코드는 없애고, export default Cart 설정 가정하에)

  1. useSelector()라는 훅을 import해온다.
import { useSelector } from 'react-redux';
  1. Cart 컴포넌트 안에 변수에 useSelector를 저장하고 콜백함수를 적자. 인자값 state는 redux에 있던 모든 state값들이고, 이 값을 리턴값으로 반환해달라는 코드이다.
    (앞에서 보여줬던 mapStateToProps는 구식)
function Cart(props) {
  let state = useSelector((state) => state)
  1. 그렇다면 state값 불러와서 써야하는 부분은 어떻게 바뀔까?

props로 변환해주는 mapStateToProps로 인해, state를 가져다 쓸때 앞에 props를 붙이고 사용하면 됬었다.
useSelector의 경우 state변수에 저장된 state값 뒤에 .reducer를 붙여주면 된다.

function Cart(props) {
  let state = useSelector((state) => state )
 {state.reducer.map((a, i) => {
          return (
            <tr key={i}>
              <td>{a.id}</td>
              <td>{a.name}</td>
              <td>{a.quan}</td>
              <td>

잠깐)
앞서 배운 mapStateToProps에서는 작명: state 는 전체 state를 받아온다는 뜻이었고, reducer가 많을 경우 각각 작명에 state.reducer라고 붙여줬었다.
useSelector의 경우도 useSelector((state) => state) 일 경우 전체 state를 받아온다는 뜻이고, useSelector((state) => state.reducer) 과 같이 작명하면 해당 reducer를 가져 오겠다는 뜻이다. 따라서, 이럴 경우 위의 코드에서 변수값 state라고만 적어도 되겠다. (state.reducer -> state)

  1. 마지막으로 dispatch 또한 방법이 있다.
    우선, import 해오자.
import { useDispatch } from "react-redux";

그리고 dispatch 변수에 저장하고, dispatch 변수를 써먹자.

useDispatch() 방법

   let dispatch = useDispatch();
<button onClick={() => {dispatch({ type: "수량증가"});}}>+</button>
                &nbsp;
                <button onClick={() => {dispatch({ type: "수량감소" });}}>-</button>

이전방법 (mapStateToProps)

<button onClick={() => {props.dispatch({ type: "수량증가"});}}>+</button>
             &nbsp;
             <button onClick={() => {props.dispatch({ type: "수량감소" });}}>-</button>

좋은 웹페이지 즐겨찾기