사상 두번째 프로젝트 - #4 useReducer + Context API

sham·2021년 12월 10일
0

Betti 개발일지

목록 보기
5/14

리액트의 컴포넌트에서는 자식 컴포넌트에게 값을 전달할 때 props로 전달하는 것이 일반적이다. 그런데 거쳐야 할 자식이 많아진다면, 쓰지도 않을 props를 계속 전달한다면 코드 가독성도 떨어지게 된다. useReducer + useContext, Redux를 이용하면 자식을 통해 props를 전달하는 구조에서 전역적으로 state에 접근하고 dispatch를 날려서 state를 변경하는 구조로 바꿀 수 있다.

Redux 라이브러리를 설치할 필요없이 react hooks인 useReducer와 useContext를 합쳐서 리덕스를 흉내내서 구현할 수 있다.

useReducer

useReducer

state와 dispatch, reducer 함수를 이용해서 리덕스를 흉내낼 수 있다.

Context API

Context API

useContext를 이용해 Context를 만들어서 컴포넌트를 감싸주면 감싸인 컴포넌트들은 Context의 value에 들어간 값에 props 없이도 접근할 수 있다.

useReducer + useContext

구현

src/contexts/Type.tsx

import { Dispatch } from 'react';

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

type teamDataType = {

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

}

export type Action =
    | { type: 'addTeam', name: string }
    | { type: 'addTest', curTeam: number, newTestName: string, curTestArray: string[] }
    | { type: 'changeTeam', dst: number }
    | { type: 'changeTest', dst: number }

export type DispatchType = Dispatch<Action>;

reducer의 인자로 들어갈 state와 Action의 타입이 될 타입들을 지정한다. 해당 타입을 import해서 사용할 것이기에 export로 내보내준다.

src/contexts/useReducer.tsx

import { StateType, Action } from './Type'

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

export const reducer = (state: StateType, action: Action): StateType => {
    const stateData = state.stateData;
    const teamData = state.teamData;
    switch (action.type) {
        case 'addTeam': {
            const newTeamIndex = teamData.length;
            const newTeamName = action.name;
            const newTeam = { index: newTeamIndex, name: newTeamName, test: [] };
            stateData.curTeam = newTeamIndex;
            state.teamData.push(newTeam);
            return { ...state };
        }
        case 'addTest': {
            console.log('실행됨!');
            const curTeamIndex = action.curTeam; // state에서 끌어다 써도 될 듯
            const testName = action.newTestName;
            const temp = [...action.curTestArray, testName];
            teamData[curTeamIndex].test = temp;
            return { ...state };
        }
        case 'changeTeam': {
            stateData.curTeam = action.dst;
            stateData.curTest = 0;
            return { ...state };
        }
        case 'changeTest': {
            stateData.curTest = action.dst;
            return { ...state };
        }
        default: {
            return state;
        }
    }
};

리듀서 부분의 코드. state에 초기값으로 들어 갈 initalState 와 dispatch를 통해 들어오는 액션을 처리하는 reducer 로 나뉘게 된다.

src/contexts/Context.tsx

import React, { createContext, useReducer, useContext } from 'react';
import { StateType, DispatchType } from './Type'
import { reducer, initalState } from "./useReducer";

const StateContext = createContext<StateType | undefined>(undefined);
const DispatchContext = createContext<DispatchType | undefined>(undefined);

// 컴포넌트에서 useContext를 사용할 때 값이 유효한지 유효하지 않은지 체크해주는 커스텀 훅을 만든다.
// 에러 체크 기능이 추가되었을 뿐 자식 컴포넌트에서 자신을 감싼 Context를 useContext로 불러온다는 점은 변함없다.
export const cusUseState = (): StateType => {
    const state = useContext(StateContext);
    if (!state) throw new Error('Provider not found');
    return state;
}

export const cusUseDispatch = (): DispatchType => {
    const dispatch = useContext(DispatchContext);
    if (!dispatch) throw new Error('Provider not found');
    return dispatch;
}

// 데이터 통신으로 받아올 것.

export const ContextProvider = ({ children }: { children: React.ReactNode }) => {
    const [state, dispatch] = useReducer(reducer, initalState);
    return (
        <DispatchContext.Provider value={dispatch}>
            <StateContext.Provider value={state}>
                {children}
            </StateContext.Provider>
        </DispatchContext.Provider>
    );
}

useContext는 useRedux로 만든 state를 실제로 사용할 컴포넌트에서 state와 dispatch를 받을 용도로 사용하는 함수인데, 그 기능에서 값이 유효한지 유효하지 않은지 체크를 하기 위해 커스텀 훅으로 만들어 준 것이다.

이제 useContext(StateContext)cusUseState로, useContext(DispatchContext)cusUseDispatch로 대체해서 사용하게 될 것이다.

createContext로 useContext의 Context를 만들어준다. 이렇게 만들어진 Context는 Provider 역할을 할 컴포넌트에 집어넣어 줄 것이다.

reducerinitalState를 하나로 합치는 ContextProvider 컴포넌트를 만든다. useReducer의 인자로 집어넣으면 리턴하는 객체를 구조 분할한다. statedispatch를 위에서 만든 Context.Provider의 value 값에 집어넣으면 해당 Provider로 감싸진 컴포넌트는 어디서든 value에 저장된 값에 접근할 수 있게된다.

인자로 받는 children은 들어올 컴포넌트, 사실상 모든 컴포넌트를 의미한다.

사용

src/App.tsx

import React from 'react';
import Routes from './Routes';
import { ContextProvider } from './contexts/Context';
import './App.scss';

function App() {
  return (
    <ContextProvider>
      <Routes />
    </ContextProvider>
  )
}

export default App;

props로 집어넣은 Routes 하위의 모든 컴포넌트는 ContextProvider로 감싸져서 useRedux로 설정한 dispatch, state에 접근할 수 있게 되었다.

src/pages/ProviderMain/Main.jsx

import { React, useEffect, useReducer } from 'react'; // useContext
import {cusUseState} from "../../contexts/Context" // StateContext
import { TeamBar, MainScreen } from '../../components';
import './Main.scss';

const Main = () => {
  const state = cusUseState(); // useContext(StateContext);
  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 Main;

Context를 만들 때 에러를 체크하는 기능을 추가한 useContext 훅을 만들어주었기에 useContext(StateContext) 와 사실상 동일한 cusUseState()를 사용해준다.

state에는 useReducer로 리턴받은 state가 담겨있다!

src/components/MainScreenBody.jsx

import React from 'react';
import { Link } from 'react-router-dom';
import {cusUseDispatch} from '../../contexts/Context';

const MainScreenBody = ({ teamData, curTeam, curTest }) => {
  const dispatch = cusUseDispatch();
  
  const addTestEvent = () => {
    dispatch({
      type: 'addTest',
      curTeam: curTeam,
      curTestArray: teamData[curTeam].test, // push로 끝에 넣을꺼니 의미 X ?
      newTestName: 'xwxa',
    });
  };
  return (
    <div className="main-screen-body">
      <div className="main-screen-tests">
        {teamData[curTeam].test.map((e, i) => {
          return (
            <div
              key={`${curTeam}-${i}`}
              className="main-screen-test"
              onClick={() => dispatch({ type: 'changeTest', dst: i })}
            >
              {e}
            </div>
          );
        })}
        <Link
          onClick={addTestEvent}
          to="/makeTest"
          className="main-screen-test add"
          style={{ textDecoration: 'none', color: 'black' }}
        >
          {' '}
          +
        </Link>
      </div>
      <div className="main-screen-test-info">
        {teamData[curTeam].test[curTest]}
      </div>
    </div>
  );
};
export default MainScreenBody;

위와 동일하게, useContext(DispatchContext) 와 동일한 cusUseDispatch()를 사용해서 useReducer로 생성한 dispatch를 가지고 온다. useRedux를 쓸 때와 동일하게 객체 형태로 타입을 설정해서 날려주면 된다.

profile
씨앗 개발자

0개의 댓글