240103 TIL

thisisyjin·2024년 1월 3일
0

TIL 📝

목록 보기
111/113

Redux

store.getState();

  • store.subscribe()
import { createStore } from 'redux';

const store = createStore(counterReducer);
const counterSubscriber = () => {
  const latestState = store.getState();
}

store.subscribe(counterSubscriber);

Reducer

  • initialState를 지정해주어야 함.
  • state는 처음에 지정되지 않기 때문
// Error - State is undefinced.
const counterReducer = (state, action) => {
  return {
    counter: state.counter + 1;
  }
}
// InitialState 지정 필요
const counterReducer = (state = { counter : 0 }, action) => {
  return {
    counter: state.counter + 1;
  }
}

store.dispatch

  • 액션 객체를 디스패치하여 state.counter을 변환시킬 수 있음.
store.dispatch({ type: 'increment' });

reducer 수정

  • 특정 action.type에 따라 다른 로직을 수행하도록 변경
const counterReducer = (state = { counter : 0 }, action) => {
  if (action.type === 'increment') {
      return {
      counter: state.counter + 1;
    }
   
  if (action.type === 'decrement') {
       return {
       counter: state.counter - 1;
     }
   }
}

Practice Project

npm install

  • redux
  • react-redux

프로젝트 생성

/src/store/index.js

import { createStore } from "redux";

const counterReducer = (state = { counter: 0 }, action) => {
  if (action.type === "increment") {
    return { counter: state.counter + 1 };
  }
  if (action.type === "decrement") {
    return { counter: state.counter - 1 };
  }
  return state;
};

const store = createStore(counterReducer);

export default store;

store Provide

  • 앱 전체를 렌더링한 index.js 파일에 가서 App 컴포넌트를 감싸줌
  • react-redux에서 Provider 컴포넌트를 임포트함.
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import store from "./store";

import "./index.css";
import App from "./App";

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

컴포넌트에서 redux 데이터 사용

  • react-redux의 useSelector 사용.
import classes from './Counter.module.css';
import { useSelector } from 'react-redux'

const Counter = () => {
  const counter = useSelector((state) => state.counter);
  const toggleCounterHandler = () => {};

  return (
    <main className={classes.counter}>
      <h1>Redux Counter</h1>
      <div className={classes.value}>{counter}</div>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;

컴포넌트에서 액션 디스패치하기

  • 예> 버튼을 클릭하면 {type: increment} 액션이 디스패치 되도록
import classes from "./Counter.module.css";
import { useSelector, useDispatch } from "react-redux";

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

  const incrementHandler = () => {
    dispatch({ type: "increment" });
  };

  const decrementHandler = () => {
    dispatch({ type: "decrement" });
  };

  const toggleCounterHandler = () => {};

  return (
    <main className={classes.counter}>
      <h1>Redux Counter</h1>
      <div className={classes.value}>{counter}</div>
      <div>
        <button onClick={incrementHandler}>Increment</button>
        <button onClick={decrementHandler}>Decrement</button>
      </div>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;

⚠️ [참고] 클래스 컴포넌트의 경우 - connect

  • useSelector, useDispatch 등 Hooks는 사용 불가함.
  • 대신 connect를 사용 가능! (두 Hook의 기능을 대체)
    • HOC (Higher-Ordered Component)
    • connect(mapStateToProps, mapDispatchToProps)(Component)
    • 두 개의 params를 받음. (1. useSelector 역할의 mapStateToProps / 2. dispatch 역할의 mapDispatchToProps)
import { connect } 'react-redux';
class Counter extends React.Component {
  // mapDispatchToProps 
  incrementHandler() {
    this.props.increment();
  }
  decrementHandler() {
    this.props.decrement();
  }
  toogleCountHandler() {}
  
  render() {
    return (
      <main className={classes.counter}>
        <h1>Redux Counter</h1>
        <div className={classes.value}>{counter}</div>
        <div>
          <button onClick={this.incrementHandler}>Increment</button>
          <button onClick={this.decrementHandler}>Decrement</button>
        </div>
        <button onClick={this.toggleCounterHandler}>Toggle Counter</button>
      </main>
    )
  }
}

const mapStateToProps = state => {
  return {
    counter: state.counter
  };
}

const mapDispatchToProps = dispatch => {
  return {
    increment: () => dispatch({type: 'increment'}),
    decrement: () => dispatch({type: 'decrement'});
  }
};

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

Payload 사용

  • 액션을 디스패치할 때, payload 라는 값을 보내줄 수 있다.
  • 리듀서 함수에서는 넘겨받은 payload값을 사용하여 state 변경 로직에 사용 가능하다.
// store/index.js
import { createStore } from "redux";

const counterReducer = (state = { counter: 0 }, action) => {
  if (action.type === "increment") {
    return { counter: state.counter + 1 };
  }
  // ✅ Payload 사용
  if (action.type === "increase") {
    return { counter: state.counter + action.value };
  }
  if (action.type === "decrement") {
    return { counter: state.counter - 1 };
  }
  return state;
};

const store = createStore(counterReducer);

export default store;
// At Component
const increaseHandler = () => {
  dispatch({ type: "increase", amount: 5 });
};

카운터 Toggle 추가하기

  • toggle 버튼을 클릭하면 카운터가 숨겨지는 기능 추가
  • 원래는 state로 진행하지만, redux를 이용한 실습 해보기.
  • 기존 counterReducer의 initialState 구조를 변경함.
import { createStore } from "redux";

const initialState = { counter: 0, showCounter: true };

const counterReducer = (state = initialState, action) => {
  if (action.type === "increment") {
    return { ...state, counter: state.counter + 1 };
  }
  if (action.type === "increase") {
    return { ...state, counter: state.counter + action.value };
  }
  if (action.type === "decrement") {
    return { ...state, counter: state.counter - 1 };
  }
  // Toggle 추가
  if (action.type === "toggle") {
    return {
      ...state,
      showCounter: !state.showCounter,
    };
  }
  return state;
};

const store = createStore(counterReducer);

export default store;
// Counter.jsx

import classes from "./Counter.module.css";
import { useSelector, useDispatch } from "react-redux";

const Counter = () => {
  const dispatch = useDispatch();
  const counter = useSelector((state) => state.counter);
  const show = useSelector((state) => state.showCounter);

  const incrementHandler = () => {
    dispatch({ type: "increment" });
  };

  const increaseHandler = () => {
    dispatch({ type: "increase", amount: 5 });
  };

  const decrementHandler = () => {
    dispatch({ type: "decrement" });
  };

  const toggleCounterHandler = () => {};

  return (
    <main className={classes.counter}>
      {show && (
        <div className="counter">
          <h1>Redux Counter</h1>
          <div className={classes.value}>{counter}</div>
          <div>
            <button onClick={incrementHandler}>Increment</button>
            <button onClick={decrementHandler}>Decrement</button>
          </div>
        </div>
      )}
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;

State 올바르게 사용하기

  • State를 관리할 때, 불변성이 보장되어야 함.
  • 기존 state를 변형시키지 않고, ...(spread)를 통해 사본을 통해 변경해야 함.

Redux Toolkit

$ npm install @reduxjs/toolkit

createSlice()

  • redux-toolkit에서 사용할 수 있는 강력한 기능.
  • createReducer()도 있지만, createSlice의 기능이 더 강력하다.
  • name(식별자)을 작성해준다.
    • ${name}Slice와 같이 사용하면 된다.
  • initialState를 지정해준다.
  • reducers 객체에는 함수가 들어가는데, 이 때는 if나 switch를 통한 action.type의 분기가 필요없어진다.
  • 또한, reducer 함수 내에서 state를 직접 변환시켜줄 수 있다.
    (물론, 코드가 동작할 때 진짜로 state가 변형되지는 않는다.)
import { createSlice } from '@reduxjs/toolkit';

createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment(state) {
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
    increase(state, action) {
      state.counter += action.amount;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    },
  },
});

createStore에 slice 연결

  • 우선, createStore()에 해당 슬라이스의 reducer을 연결시켜준다.
  • 주의할 것은, reducers가 아닌 reducer을 넣어줘야 한다.
  • 이 것은 하나의 큰 리듀서 함수를 의미한다.
import { createStore } from "redux";
import { createSlice } from "@reduxjs/toolkit";

const initialState = { counter: 0, showCounter: true };

createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment(state) {
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
    increase(state, action) {
      state.counter += action.amount;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    },
  },
});

const store = createStore(counterSlice.reducer);

export default store;
  • 그러나, 지금 여러개의 리듀서 함수들이 있기 때문에 combineStore을 해줘야 함.

[참고] combineReducers

  • redux 라이브러리의 기능으로, 여러 리듀서를 하나의 루트 리듀서로 합쳐줌.
  • 참고 문서
  • 기존 redux의 createStore을 이용하는 경우에는 위와 같이 combineReducers를 해주어야 하는 불편함이 있었음.
  • 그러나, redux-toolkit의 configureStore을 사용해주면 따로 combineReducers를 사용할 필요 X.

ConfigureStore

  • 참고 문서에 의하면
  • Combining the slice reducers into the root reducer 기능이 있다.
// (common) slice가 여러개인 경우, reducer.counter 안에 넣어줘야 함.
const store = configureStore({
  reducer: { counter: counterSlice.reducer }
});
  • useReducer을 사용한 경우, 아래와 같이 사용함.
const store = configureStore({
  reducer: {
    todos: todosReducer,
    auth: authReducer
  }
});

state 연결하기

import { createSlice, configureStore } from "@reduxjs/toolkit";

const initialState = { counter: 0, showCounter: true };

createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment(state) {
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
    increase(state, action) {
      state.counter += action.payload;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    },
  },
});

const store = configureStore({
  reducer: counterSlice.reducer,
});

export default store;

Redux-toolkit 마이그레이션

액션 생성자

  • 액션 type에 해당하는 것을 전달하기 위해선 ${name}Slice.actions.${reducerName}을 호출하면 된다.
  • 즉, 아래 메서드를 호출하면 액션 객체가 생성되므로, '액션 생성자' 라고 한다.
  • 직접 액션을 생성할 필요가 없고, actions를 사용하면 됨.
counterSlice.actions.increase();
// return { type: 'some identifier' } 
  • 액션 객체를 생성할 필요 X
  • 고유 type명 생각할 필요 X

actions export

export const counterActions = counterSlice.actions;
  • 해당 슬라이스의 actions를 export 하고,
  • 액션을 디스패치 하고 싶은 곳에서 import해서 메서드명을 붙여 사용하면 된다.
    예>
import { counterActions } from "../store/index";

...

const incrementHandler = () => {
  dispatch(counterActions.increment());
};

const decrementHandler = () => {
  dispatch(counterActions.decrement());
};

const increaseHandler = () => {
  dispatch(counterActions.increase(5)); // action.payload
};

[주의] action.payload

  • 이전에는 아래와 같이 payload 필드의 이름을 지정할 수 있었지만,
  • redux-toolkit에서는 액션을 자동으로 생성하고, 추가 데이터는 payload라는 이름으로 받아오기 때문에 임의로 필드명을 수정할 수 없다.
// 1. 기존 방식
const increaseHandler = () => {
  dispatch({ type: "increase", amount: 5 });
};

// 2. Redux-Toolkit (Slice)
const incrementHandler = () => {
  dispatch(counterActions.increase(5));
};
// 1. 기존 방식
if (action.type === "increase") {
  return { counter: state.counter + action.amount };
}

// 2. Reduxt-Toolkit
increase(state, action) {
  state.counter += action.payload;
},

다중 Slice 작업

  1. 컴포넌트 추가
// App.js
import Counter from "./components/Counter";
import Header from "./components/Header";
import Auth from "./components/Auth";
import { Fragment } from "react";

function App() {
  return (
    <Fragment>
      <Header />
      <Auth />
      <Counter />
    </Fragment>
  );
}

export default App;
  1. store/index.js 수정
  • 기존 상태
import { createSlice, configureStore } from "@reduxjs/toolkit";

const initialState = { counter: 0, showCounter: true };

const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment(state) {
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
    increase(state, action) {
      state.counter += action.payload;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    },
  },
});

const store = configureStore({
  reducer: { counter: counterSlice.reducer },
});

export const counterActions = counterSlice.actions;

export default store;
  • initialAuthState 추가 (initialState 분리)
  • authSlice 생성
  • configureStore 변경
import { createSlice, configureStore } from "@reduxjs/toolkit";

const initialCounterState = { counter: 0, showCounter: true };
const initialAuthState = {
  isAuthenticated: false,
};

const counterSlice = createSlice({
  name: "counter",
  initialState: initialCounterState,
  reducers: {
    increment(state) {
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
    increase(state, action) {
      state.counter += action.payload;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    },
  },
});

const authSlice = createSlice({
  name: "auth",
  initialState: initialAuthState,
  reducers: {
    login(state) {
      state.isAuthenticated = true;
    },
    logout(state) {
      state.isAuthenticated = false;
    },
  },
});

const store = configureStore({
  reducer: { counter: counterSlice.reducer, auth: authSlice.reducer },
});

export const counterActions = counterSlice.actions;
export const authActions = authSlice.actions;

export default store;
  • 리듀서가 여러개이기 때문에, 값을 읽어 들일 때에도 코드 변경 필요.
// 기존 코드
const counter = useSelector(state => state.counter);

// 변경 코드
const counter = useSelector({counter} => counter.counter);

Action Dispatch

// Auth.jsx
import classes from "./Auth.module.css";
import { useDispatch } from "react-redux";
import { authActions } from "../store/index";

const Auth = () => {
  const dispatch = useDispatch();

  const handleLogin = () => {
    dispatch(authActions.login());
  };

  return (
    <main className={classes.auth}>
      <section>
        <form>
          <div className={classes.control}>
            <label htmlFor="email">Email</label>
            <input type="email" id="email" />
          </div>
          <div className={classes.control}>
            <label htmlFor="password">Password</label>
            <input type="password" id="password" />
          </div>
          <button onClick={handleLogin}>Login</button>
        </form>
      </section>
    </main>
  );
};

export default Auth;
// Header.jsx
import classes from "./Header.module.css";
import { useDispatch } from "react-redux";
import { authActions } from "../store/index";

const Header = () => {
  const dispatch = useDispatch();
  const handleLogout = () => {
    dispatch(authActions.logout());
  }
  
  return (
    <header className={classes.header}>
      <h1>Redux Auth</h1>
      <nav>
        <ul>
          <li>
            <a href="/">My Products</a>
          </li>
          <li>
            <a href="/">My Sales</a>
          </li>
          <li>
            <button onClick={handleLogout}>Logout</button>
          </li>
        </ul>
      </nav>
    </header>
  );
};

export default Header;

useSelector

// App.js
import { useSelector } from "react-redux";

import Counter from "./components/Counter";
import Header from "./components/Header";
import Auth from "./components/Auth";
import UserProfile from "./components/UserProfile";
import { Fragment } from "react";

function App() {
  const isLogined = useSelector((state) => state.auth.isAuthenticated);

  return (
    <Fragment>
      <Header />
      {isLogined ? <UserProfile /> : <Auth />}
      <Counter />
    </Fragment>
  );
}

export default App;
// Header.jsx

import classes from "./Header.module.css";
import { useDispatch, useSelector } from "react-redux";
import { authActions } from "../store/index";

const Header = () => {
  const dispatch = useDispatch();
  const isLogined = useSelector((state) => state.auth.isAuthenticated);

  const handleLogout = () => {
    dispatch(authActions.logout());
  };

  return (
    <header className={classes.header}>
      <h1>Redux Auth</h1>
      {isLogined && (
        <nav>
          <ul>
            <li>
              <a href="/">My Products</a>
            </li>
            <li>
              <a href="/">My Sales</a>
            </li>
            <li>
              <button onClick={handleLogout}>Logout</button>
            </li>
          </ul>
        </nav>
      )}
    </header>
  );
};

export default Header;

코드 분할

/store/index.js 안에 다 작성되어있는 코드를 분할해보자.

  • /store/counter.js
  • /store/auth.js
// store/index.js
import { configureStore } from "@reduxjs/toolkit";
import counterSlice from "./counter";
import authSlice from "./auth";

const store = configureStore({
  reducer: { counter: counterSlice.reducer, auth: authSlice.reducer },
});

export default store;
// store/counter.js
import { createSlice } from "@reduxjs/toolkit";

const initialCounterState = { counter: 0, showCounter: true };

const counterSlice = createSlice({
  name: "counter",
  initialState: initialCounterState,
  reducers: {
    increment(state) {
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
    increase(state, action) {
      state.counter += action.payload;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    },
  },
});

export const counterActions = counterSlice.actions;
export default counterSlice;
// store/auth.js
import { createSlice } from "@reduxjs/toolkit";

const initialAuthState = {
  isAuthenticated: false,
};

const authSlice = createSlice({
  name: "auth",
  initialState: initialAuthState,
  reducers: {
    login(state) {
      state.isAuthenticated = true;
    },
    logout(state) {
      state.isAuthenticated = false;
    },
  },
});

export const authActions = authSlice.actions;
export default authSlice;
profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글