241120 리액트 (Redux)

241120 에이콘 아카데미 수업에 기반하여 작성되었음을 알립니다.

리액트 (Redux)

오늘의 리덕스 이야기!

출처 : 개발일지 S_Soo100 : [React-Redux] 개념부터 기본 사용법 까지 

자식 컴포넌트내의 데이터를 자식 컴포넌트끼리 전송하면 안된다.
부모 컴포넌트로 전송하고 하위 컴포넌트로 내려줘야 리액트에서 일관성이 보장된다.
컴포넌트 수가 적다면 리덕스를 굳이 사용을 하지 않아도 크게 문제가 되지 않겠지만, 실제 프로젝트에서는 컴포넌트가 수없이 많기 때문에 이렇게 state값을 컴포넌트를 타고 넘기는 일들은 보통 힘든 일이 아닐 수 없다.

그렇기 때문에 변수들을 담을 수 있는 창고(store)를 만들어 담아두면 좋겠다. 창고에 있는 변수들을 가져다 쓰면 부모 - 자식 컴포넌트 사이에서 데이터를 넘기지 않아도 괜찮다. 이러한 방식이 리덕스 라이브러리이다.

각각의 자식 컴포넌트에서 연산을 하고 있다고 가정한다. 이 때 스토어의 state 값을 문자열로 바꾸게 된다면 자식 컴포넌트 내에서 에러가 떨어질 것이다. 그래서 자식 컴포넌트에서는 계산을 하지 않고 스토어 내부에서 계산을 진행하는 타입을 만들어 준다. 스토어에서 각각의 컴포넌트에서 사용할 state를 선언하고 계산도 해준다. 이러한 것들을 리듀서라고한다. 컴포넌트에서는 스토어의 state를 참조만 하면 될 것이다.

아래 두 가지 개념만 우선적으로 알고있으면 되겠다.

useSelector - 데이터 읽기
store에 저장된 내용을 read하기 위해서는 useSelector훅을 사용한다. useSelector훅은 리덕스에서 자체 제공한다.

dispatch - 데이터 수정
redux의 기본 제공 hook으로, useSelector가 state값을 read하기 위해서 사용되었다면, 이 hook은 state값을 변경하기 위해서 사용된다.

리덕스의 실행 원칙, 아키텍쳐, 라이프사이클, 장점, 단점을 잘 정리해보자!
https://www.frontoverflow.com/document/1/%EC%B2%98%EC%9D%8C%20%EB%A7%8C%EB%82%9C%20%EB%A6%AC%EB%8D%95%EC%8A%A4%20(Redux)/chapter/2/Redux%20%EC%86%8C%EA%B0%9C/section/4/Redux%EC%9D%98%20%ED%83%84%EC%83%9D

리덕스 실습 (리덕스 사용 X)

리덕스 사용 전 먼저 컴포넌트의 단계를 타고 데이터를 넘기는 과정을 알아보자!

App.js

import { useState } from 'react';
import './App.css';
import AddNumberSuper from './mydir/AddNumberSuper';
import ShowNumberSuper from './mydir/ShowNumberSuper';

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

  const handleAddNumber = (size) => {
    setNumber(number + size); // state를 size만큼 증가시켜 update하고 있다.
  }

  return (
    <div className="App">
      <h1>메인</h1>
      <AddNumberSuper onClick={handleAddNumber} />
      AddNumber : {number}
      <br />
      <ShowNumberSuper number={number} />
    </div>
  );
}

export default App;

AddNumber.js

import React, { useState } from "react";

const AddNumber = (props) => {
    const [size, setSize] = useState(1);

    const handleClick = () => {
        props.onClick(size);
    }

    const handleChange = (e) => {
        setSize(Number(e.target.value));
    }

    return(
        <div>
            <h1>Add Number</h1>
            <input type="button" value="+" onClick={handleClick} />
            <input type="text" value={size} onChange={handleChange}/>
        </div>
    );
};

export default AddNumber;

AddNumberSuper.js

import React from "react";
import AddNumber from "./AddNumber";


const AddNumberSuper = (props) => {
    return(
        <div id="super">
            <h1>Add Number Super</h1>
            <AddNumber onClick={(size) => props.onClick(size)} />
        </div>
    );
};

export default AddNumberSuper;

ShowNumber.js

import React from "react";

const ShowNumber = (props) => {
    return(
        <div>
            <h1>Show Number</h1>
            <input type="text" value={props.number} readOnly />
        </div>
    );
};

export default ShowNumber;

ShowNumberSuper.js

import React from "react";
import ShowNumber from "./ShowNumber";

const ShowNumberSuper = (props) => {
    return(
        <div id="super">
            <h1>Show Number Super</h1>
            Show Number Super : {props.number}
            <ShowNumber number={props.number} />
        </div>
    );
};

export default ShowNumberSuper;

결과

초기 메인화면

Add Number의 증가시킨 state 값을 ShowNumber에 넘겨보자!
AddNumber >  AddNumberSuper > Main > ShowNumberSuper > ShowNumber
리덕스를 사용하지 않는다면 리액트 컴포넌트 트리 구조의 단계를 거쳐야 한다!

AddNumber 증가


AddNumberSuper > Main > ShowNumberSuper > ShowNumber 데이터가 넘겨진 것을 확인할 수 있다.

리덕스 라이브러리를 사용한다면 이러한 과정 없이 스토어에 각각의 state를 만들어 스토어 내에서 state의 상태값이 변경되도록 계산식들을 만들고 타입(이름)을 준다. 이렇게 state의 값을 변경하는 계산식들을 리듀서라 부르고, 각각의 컴포넌트에 가져다 쓰면 된다. (useSelector)
스토어(state, 상태), trigger, subscribe(어플리케이션에 업데이트된 state 값을 컴포넌트에 다시 전달)
⬆️
리듀서 (함수) :상태와 액션을 받아 갱신 작업을 거친 후 스토어의 state를 업데이트 시킨다.
⬆️
action type / data를 전달
⬆️
Action
⬆️
dispatch : state의 변경을 요청
⬆️
컴포넌트(뷰)

그림을 그려보자!!
출철 : https://dev-astra.tistory.com/550

리덕스 실습 (리덕스 사용)

리덕스 라이브러리 설치

npm install redux
npm install react-redux
npm install @reduxjs/toolkit // 최신 버전의 리덕스 사용
npm install @reduxjs/toolkit react-redux // 한꺼번에 쓸 수도 있다.

index.js

<Provider store={store}></Provider>로 <App />을 감싸주자!

(이렇게 하라고 형식을 만들어줌, 그대로 따라하자)

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import store from "./store";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
    <App />
    </Provider>
  </React.StrictMode>
);

// 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();

store.js

src/store.js 생성, 리듀서 등록을 하기 위함

import { configureStore } from "@reduxjs/toolkit";
import { numberReducer } from "./reducer";

const store = configureStore({ // 리듀서 등록
    reducer:numberReducer, // 객체 형태로 나열만 해주면 된다.
});

export default store;

reducer.js

// 리덕스의 리듀서 함수 : Action에 따라 변경하는 순수함수로
// 현재 state와 action을 받아 새로운 상태로 변환

const initialState = {number:0}; // 리듀서의 초기 상태 설정

export const numberReducer = (state=initialState, action) => { // state, action 값이 들어옴
    switch(action.type) {
        case "INCREASE_NUMBER":
            return{...state, number:state.number + action.payload}; // 리듀서로 전달되는 payload의 값만큼 state에 변화를 준다.
        default:
            return state;
    }
};

App.js

리덕스 사용시 App.js (메인)에서는 리덕스의 스토어를 사용하지 않을 것이기에 state를 신경쓸 필요가 없다.

import './App.css';
import AddNumberSuper from './mydir/AddNumberSuper';
import ShowNumberSuper from './mydir/ShowNumberSuper';

function App() {
  return (
    <div className="App">
      <h1>메인(리덕스 사용)</h1>
      <AddNumberSuper />
      <br />
      <ShowNumberSuper />
    </div>
  );
}

export default App;

AddNumber.js (위와 동일)

import React, { useState } from "react";

const AddNumber = (props) => {
    const [size, setSize] = useState(1);

    const handleClick = () => {
        props.onClick(size);
    }

    const handleChange = (e) => {
        setSize(Number(e.target.value));
    }

    return(
        <div>
            <h1>Add Number</h1>
            <input type="button" value="+" onClick={handleClick} />
            <input type="text" value={size} onChange={handleChange}/>
        </div>
    );
};

export default AddNumber;

AddNumberSuper.js

import React from "react";
import AddNumber from "./AddNumber";
import { useDispatch } from "react-redux"; // Hook의 일종, 리덕스를 지원한다.

// const AddNumberSuper = (props) => {
const AddNumberSuper = () => { // 리덕스 사용해서 부모로부터 받을 데이터가 없다.
    const dispatch = useDispatch(); // 컴포넌트 내에서 디스패치를 사용할 수 있도록 초기화 작업 진행

    // type : Redux(reducer)에서 실행할 작업의 종류를 결정 (보통 문자열로 준다.)
    // payload : store의 state를 업데이트할 데이터(값)
    const handleClick = (size) => {
        dispatch({type:"INCREASE_NUMBER", payload:size}); // Aciton의 type과 payload를 주어 리듀서의 계산 로직 호출
    }

    return(
        <div id="super">
            <h1>Add Number Super</h1>
            <AddNumber onClick={handleClick} />
        </div>
    );
};

export default AddNumberSuper;

ShowNumber.js (위와 동일)

import React from "react";

const ShowNumber = (props) => {
    return(
        <div>
            <h1>Show Number</h1>
            <input type="text" value={props.number} readOnly />
        </div>
    );
};

export default ShowNumber;

ShowNumberSuper.js

import React from "react";
import ShowNumber from "./ShowNumber";
import { useSelector } from "react-redux"; // 데이터 읽기

//const ShowNumberSuper = (props) => { 메인에서 넘어오는 데이터가 없음!
const ShowNumberSuper = () => {
    // useSelector로 redux의 상태(state)를 가져온다.
    // store의 상태(state)가 변경되면, useSelector를 사용하는 모든 컴포넌트도 리렌더링된다.
    const number = useSelector((state) => state.number); // store의 state 호출
    return(
        <div id="super">
            <h1>Show Number Super</h1>
            Show Number Super : {number}
            <ShowNumber number={number} />
        </div>
    );
};

export default ShowNumberSuper;

결과


리덕스 실습 (ResourceSlice 이용)

                부모 컴포넌트
자식 컴포넌트A, 자식 컴포넌트B

slice를 사용하여 A, B 컴포넌트에서 각각의 값을 공유해보자!

ResourceSlice.js (리듀서)

// reducer 파일
import { createSlice } from "@reduxjs/toolkit";

// createSlice() : 리듀서와 액션을 생성, 초기 상태 정의, 함수 관리, 불변성 관리
const ResourceSlice = createSlice({
    name:"resource",
    initialState:{ // state 초기값
        value:0, // 공유 자원
        kor:50, // ...

    },
    // 리듀서(reducer) : 스토어의 상태(state)를 변경할 수 있는 함수들의 집합
    reducers:{ // 리듀서를 정의, 각 함수는 state, action을 인수로 받는다.
        increment:(state) => {
            state.value += 1;
        },
        decrement:(state) => {
            state.value -= 1;
        },
        reset:(state) => {
            state.value = 0;
        },
        // 3개의 action type을 만들어 줌, 계속 만들 수 있음 ...
    },
    // 계속 만들 수 있음 ...
});

// 액션, 리듀서 내보내기
export const {increment, decrement, reset} = ResourceSlice.actions;
export default ResourceSlice.reducer;

Store.js (스토어)

import { configureStore } from "@reduxjs/toolkit";
import resourceReducer from "./ResourceSlice";

const Store = configureStore({
    reducer:{
        resource:resourceReducer,

    }
});

export default Store;

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import Store from './redux/Store';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={Store} >
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </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();

App.js

import React from 'react';
import Parent from './Parent';

function App() {
  return (
    <div className="App">
      <h1>* 자식 컴포넌트 간 자원 공유 *</h1>
      <Parent />
    </div>
  );
}

export default App;

Parent.js

import React from "react";
import ChildA from "./ChildA";
import ChildB from "./ChildB";

function Parent() {
    return(
        <div>
            <h2>부모 컴포넌트</h2>
            <ChildA />
            <ChildB />
        </div>
    )
}

export default Parent;

ChildA.js

import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, reset } from "./redux/ResourceSlice";

export default function ChildA() {
    const value = useSelector((state) => state.resource.value);
    const dispatch = useDispatch();

    return(
        <div>
            <h3>Child A</h3>
            <p>공유 값 : {value}</p>
            <button onClick={() => dispatch(increment())}>값(value) 증가</button>
            <button onClick={() => dispatch(reset())}>값(value) 초기화</button>
        </div>
    );
}

ChildB.js

import { useDispatch, useSelector } from "react-redux";
import { increment, decrement, reset } from "./redux/ResourceSlice";

export default function ChildB() {
    const storeValue = useSelector((state) => state.resource.value);
    const dispatch = useDispatch();

    return(
        <div>
            <h3>Child B</h3>
            <p>리덕스 스토어내의 state 값 value는 {storeValue}</p>
            <button onClick={() => dispatch(decrement())}>state 값 value 감소</button>
        </div>
    );
}

결과

 


문제 (장바구니)

😍 문제 : Redux 사용해 쇼핑 장바구니 구현하기 -  어떤 쇼핑몰에서 사용자가 상품을 장바구니에 추가하거나 제거할 수 있는 기능을 구현하려 한다.  아래 요구사항을 충족하도록 리덕스와 리액트를 활용한 project를 구현하시오.요구사항:
1) 리덕스 상태 구성
   - cart라는 배열 상태를 유지해야 한다.
   - 각 상품 객체는 { id, name, price, quantity } 형식이다.
   - 초기 상태는 빈 배열이다.
const products = [
    { id: 1, name: "마웃스", price: 5000 },
    { id: 2, name: "키보드", price: 50000 },
    { id: 3, name: "모니터", price: 500000 }
,];
2) 액션 정의
   상품 추가: ADD_TO_CART,  상품 제거: REMOVE_FROM_CART,  수량 변경: UPDATE_QUANTITY
3) 리듀서 구현 : 액션에 따라 cart 상태를 업데이트하시오.
   상품이 없으면 추가하고, 이미 있으면 수량을 업데이트한다. 상품 제거 시 해당 id를 가진 상품을 삭제한다.
4) 컴포넌트 구현
   - 상품 목록을 보여주고, 각 상품에 대해 "장바구니에 추가" 버튼을 구현.
   - 장바구니에 담긴 상품 리스트를 보여주는 컴포넌트를 작성.
   - 각 상품에 대해 수량을 증가/감소하는 버튼과 삭제 버튼 작성.
5) 리덕스 훅 사용 : useSelector와 useDispatch를 활용.
6) 장바구니에 담긴 상품 총 금액을 계산하는 getTotalPrice를 구현하시오.

라우터를 이용해서 나누기!

 

에러

정리 예정

 

결과

라우터 미적용

 

라우터 적용