사상 두번째 프로젝트 - #5 Redux

sham·2021년 12월 10일
0

Betti 개발일지

목록 보기
6/14

useReducer + Context API로 Redux를 흉내내보았으니, 이제 진짜 Redux를 구현해볼 차례이다.

리덕스의 특징

  1. 하나의 스토어
  • 여러 가지 스토어를 하나의 action로 묶어서 사용하는데, 보통 rootReducer로 작성한다.
  1. state는 읽기 전용

statedispatchaction을 보내야만 변화할 수 있다. Reducer 함수 이외의 곳, state의 값을 받아서 사용하는 컴포넌트에서는 읽기만 할 수 있는 것이다.

  1. Reducer는 순수 함수

Reducer 함수는 인자로 statedispatch의 인자인 action을 받는다. state를 변화시키고 리턴할 때, 새로운 객체로 리턴해야 한다. 보통 {...state} 문법으로 새로운 객체를 만든다.

리덕스와 useReducer + Context API의 차이

[React ] Redux와 useReducer의 차이, Context API 사용하기 — @Devme (tistory.com)
https://ridicorp.com/story/how-to-use-redux-in-ridi/

  1. 리덕스는 dispatch를 통해 state가 동기적으로 변경, useReducer는 비동기적으로 변경된다고 한다.
  2. 컴포넌트에서 특정 값을 의존할 때 리덕스에서는 해당 값이 바뀔 때에만 리렌더링을 하지만 Context API에서는 쓸데없는 리렌더링이 발생하게 된다.

리덕스 구조

링크를 참조.

리덕스 구현

액션, 액션 함수, 리듀서를 하나의 파일에서 관리하는 ducks 패턴을 이용해서 구현하였다.

src/modules/Provider.ts

// 액션 타입 선언

const CHANGE_TEAM = 'Provider/CHANGE_TEAM' as const;
const CHANGE_TEST = 'Provider/CHANGE_TEST' as const;
const ADD_TEAM = 'Provider/ADD_TEAM' as const;
const ADD_TEST = 'Provider/ADD_TEST' as const;

// 액션 생성 함수 선언

export const changeTeam = (dst: number) => ({
    type: CHANGE_TEAM,
    payload: dst
})

export const changeTest = (dst: number) => ({
    type: CHANGE_TEST,
    payload: dst
})
export const addTeam = (name: string) => ({
    type: ADD_TEAM,
    payload: name
});

export const addTest = (curTeam: number, curTestArray: string[], newTestName: string) => ({
    type: ADD_TEST,
    payload: {
        curTeam, newTestName, curTestArray
    }
});

// 액션 객체들에 대한 타입 설정

export type curStateAction =
    | ReturnType<typeof changeTeam>
    | ReturnType<typeof changeTest>
    | ReturnType<typeof addTeam>
    | ReturnType<typeof addTest>

// 상태의 타입 및 초깃값 선언

export type teamDataType = {

    index: number,
    name: string,
    test: string[]
}

export type StateType = {
    stateData: {
        curTeam: number,
        curTest: number,
    },
    teamData: teamDataType[]
};

const initalState: StateType = {
    stateData: {
        curTeam: 0,
        curTest: 0,
    },
    teamData: [
        { index: 0, name: 'asg Team', test: ['qwre', 'asdg', 'asd'] }, // 각 테스트는 객체 형태여야 함.
        { index: 1, name: 'qwer Team', test: ['test1', 'test2', 'test3'] },
    ],
};

// 리듀서 선언

const Provider = (state: StateType = initalState, action: curStateAction) => {
    const stateData = state.stateData;
    const teamData = state.teamData;
    switch (action.type) {
        case ADD_TEAM: {
            const newTeamIndex = teamData.length;
            const newTeamName = action.payload;
            const newTeam = { index: newTeamIndex, name: newTeamName, test: [] };
            stateData.curTeam = newTeamIndex;
            state.teamData.push(newTeam);
            return { ...state };
        }
        case ADD_TEST: {
            console.log('실행됨!');
            const curTeamIndex = action.payload.curTeam; // state에서 끌어다 써도 될 듯
            const testName = action.payload.newTestName;
            const temp = [...action.payload.curTestArray, testName];
            teamData[curTeamIndex].test = temp;
            return { ...state };
        }
        case CHANGE_TEAM: {
            stateData.curTeam = action.payload;
            stateData.curTest = 0;
            return { ...state };
        }
        case CHANGE_TEST: {
            stateData.curTest = action.payload;
            return { ...state };
        }
        default: {
            return state;
        }
    }

}

export default Provider;

액션들의 이름이 중복되지 않게 리듀서의 이름을 앞에 붙인다. 액션 생성 함수들과 모든 액션 생성 함수들에 해당하는 타입, 상태의 타입과 초깃값, 리듀서 함수를 선언한다. 기본적인 리듀서 파일에 타입이 붙은 것 뿐, 큰 틀은 리듀서와 변함이 없다.

src/modules/index.ts

import { combineReducers } from 'redux';
import Provider from './Provider';
import Login from './Login';

const rootReducer = combineReducers({
    Provider
});

export default rootReducer;

export type RootState = ReturnType<typeof rootReducer>;

모든 리듀서들을 하나의 리듀서로 묶어주고 내보내준다. RootState는 컴포넌트에서 useState, useDispatch로 리듀서를 사용하게 될 때 인자인 state의 타입을 위해 선언해준다.

src/index.tsx

import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './modules';
import { composeWithDevTools } from 'redux-devtools-extension'; // 리덕스 개발자 도구
import 'semantic-ui-css/semantic.min.css';

const store = createStore(rootReducer, composeWithDevTools());

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root'),
);

위에서 선언된 rootReducer가 Provider의 store로 저장된다. App에 포함되는, 즉 모든 컴포넌트가 useSelectoruseDispatch를 통해 statedispatch에 접근할 수 있게 된 것이다.

src/pages/ProviderMainPage/ProviderMainPage.tsx

import { useSelector } from 'react-redux';
import { RootState } from '../../modules';
import { TeamBar, MainScreen } from '../../components/index';

import './Main.scss';

const ProviderMainPage = () => {
  const state = useSelector((state: RootState) => state.Provider);
  const curTeam = state.stateData.curTeam;
  const curTest = state.stateData.curTest;
  const teamData = state.teamData;

  return (
    <>
      {teamData && (
        <div className="page-main">
          <TeamBar teamData={teamData} />
          <MainScreen teamData={teamData} curTeam={curTeam} curTest={curTest} />
        </div>
      )}
    </>
  );
};

export default ProviderMainPage;

src/modules/index.ts 에서 선언한 RootState로 전체 state의 타입을 지정해주고 나서야 state를 사용할 수 있다. 저 타입 지정이 되지 않으면 렌더링 자체가 실패하게 된다...

src/pages/LoginPage/LoginPage.tsx

import { Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { toggleLogin } from "../../modules/Login"

import { LoginForms } from '../../components';

const LoginPage: React.FC = () => {
  const dispatch = useDispatch();

  const handleLogin = () => {
    dispatch(toggleLogin());
  }
  return (
    <>
      <div className="page-login">
        <LoginForms />
      </div>
      <Link to="pro">
        <div>to pro</div>  
      </Link>
      <div onClick={handleLogin}>로그인하기</div>

      <Link to="use">
        <div>to use</div>
      </Link>
    </>
  );
};

export default LoginPage;

dispatch의 경우 useDispatch 말고도 사용하려는 액션 함수도 같이 끌어와야만 한다.

profile
씨앗 개발자

0개의 댓글