ContextProvider를 사용할때 Route에는 어떻게 적용하는게 효율적일지!

<Route path="/app/dashboard">
  <DashboardContext>
    <Dashboard></Dashboard>
  </DashboardContext>
</Route>

반복적으로 위와같이 입력하기 어려우니 ContextRoute를 생성하자


import React from 'react';
import { Route } from 'react-router-dom';

const ContextRoute = ({ provider, component, ...rest }) => {
  const { Provider } = provider;
  const Component = component;

  return (
    <Route {...rest}>
      <Provider>
        <Component />
      </Provider>
    </Route>
  );
};

export default ContextRoute; 

위에 ContextRoute를 정의한 이후에는 다음과같이 작성하면 된다.

<ContextRoute
  path="/app/dashboard"
  provider={DashboardContext}
component={Dashboard}
/>

참고

React Context는 오랫동안 사용이 가능했지만 이번에 React hooks이 나오면서 더 좋아졌다. 이전에는 redux라는 third-party libraries를 사용해서 React Apps의 상태를 관리했었다. 이 개발자가 redux에 대한 내용 을 작성했는데, redux를 사용할 개발자들은 이 해당 포스트를 참고해도 좋을것 같다. (참고로 이 코드를 React context hook 으로 리팩토링을 진행했음

React Redux를 사용할때

나는 react-context-hook을 사용하고 싶으니... 이 코드의 흐름을 이해해보려고 한다. 이전 redux를 사용할때는 component, container, ilb/api, modules를 각각 생성했었는데, 코드 관리가 어려웠다. 하나의 API를 추가하려면 너무 많은 작업이 필요했고, 여러 파일을 변경하면서 수정하는게 보통 쉬운게 아니다. 내가 모듈에 작성했던건지.. lib/api에 작성했던건지 나도 기억이 안나니.. 보통 Redux를 사용하면 Component에 내가 원하는 UI를 표시하고, Container안에 component를 넣고 container에서는 데이터를 lib/api, modules을 통해 가져온다. 역할로 나눠보면 lib/api, modules은 데이터 처리를 해주고, container에서는 데이터 처리의 상태를 확인하다가 변경된 부분이 있다면 데이터를 업데이트를하고, component에서는 container에서 업데이트된 데이터를 화면에 출력을 한다.

위 작업을 하기 위해서는 첫번째로 component를 작성하고, 두번째로 lib/api, module을 작성을 한뒤에 컴포넌트에 데이터가 정상적으로 들어가는지 확인하고, 이후에는 변경되는 데이터를 확인하고 업데이트하는 container를 작성해 최종적으로 component가 아닌 container를 pages에 추가했었다. 이방법이 정답은 아니겠지만 내가 본 책에서는 그랬다. 지금 다른 코드를 보면 이 책도 예전 방식을 따라했던것 같다. 너무 빠르게 변화함..

React Context를 사용하면

React Context를 사용하면 context, container, component를 생성하면된다.

  • Context
    • reducer를 구현해 어떤 액션 타입이 왔을때 어떻게 데이터를 리턴할지에 대해서 명시
    • 추가로 article에 대한 articleContext.js를 생성하고, 그 안에는 ArticleProvider를 정의한다. 이 Provider는 값을 전달하는 역할을 한다.
  • Container
    • container는 context를 이용해 article의 내용을 가져오는 역할을 한다. useContext(ArticleContext를 통해 articles을 가져오고 article을 화면에 출력할 내용을 생성한다. (출력할때 component에 미리 Article 을 만들어놓으면 여기서 사용)
  • Component
    • 위 container에서 작업할 Article을 만든다.
    • ArticleAdd.js를 생성하는데 여기서도 useContext(ArticleContext)을 이용해 dispatch를 가져온다.
      • newAddArticle과 같은 함수를 호출한다.
      • 호출되는 함수에서는 dispatch를 통해 새로운 Article이 추가되었다고 알린다.
  • App.js
    • 최종 앱에는 아래 같은 하위 구성을 정의한다.
      • ArticleProvider (context)
        • AddArticle (component)
        • Articles (container)

참고

React에서 index.js에 Provider로 ReactDOM을 포장을 하는데 그 이유가 이제 전체 App에서 Provider가 제공하는 값들을 사용하기 위해서이다. 보통 LayoutProvider, UserProvider, ThemeProvider를 생성하게 된다.

UserProvider 에서는 사용자의 상태를 관리하고 있어서 App.js에서는 UserProvider를 이용해서 사용자의 로그인 상태를 가져온다. 이렇게 가져온 상태를 통해서 App의 화면을 어떤것을 보여줄지에 대해서 설명한다.

index.js

ReactDOM.render(
  <LayoutProvider>
    <Provider store={store}>
      <ThemeProvider theme={Themes.default}>
        <BrowserRouter>
          <HelmetProvider>
            <App />
          </HelmetProvider>
        </BrowserRouter>
      </ThemeProvider>
    </Provider>
  </LayoutProvider>,
  document.getElementById('root'),
);

src/context/LayoutContext.js

import React from 'react';

var LayoutStateContext = React.createContext();
var LayoutDispatchContext = React.createContext();

function layoutReducer(state, action) {
  switch (action.type) {
    case 'TOGGLE_SIDEBAR':
      return { ...state, isSidebarOpened: !state.isSidebarOpened };
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

function LayoutProvider({ children }) {
  var [state, dispatch] = React.useReducer(layoutReducer, {
    isSidebarOpened: true,
  });
  return (
    <LayoutStateContext.Provider value={state}>
      <LayoutDispatchContext.Provider value={dispatch}>
        {children}
      </LayoutDispatchContext.Provider>
    </LayoutStateContext.Provider>
  );
}

function useLayoutState() {
  var context = React.useContext(LayoutStateContext);
  if (context === undefined) {
    throw new Error('useLayoutState must be used within a LayoutProvider');
  }
  return context;
}

function useLayoutDispatch() {
  var context = React.useContext(LayoutDispatchContext);
  if (context === undefined) {
    throw new Error('useLayoutDispatch must be used within a LayoutProvider');
  }
  return context;
}

export { LayoutProvider, useLayoutState, useLayoutDispatch, toggleSidebar };

// ###########################################################
function toggleSidebar(dispatch) {
  dispatch({
    type: 'TOGGLE_SIDEBAR',
  });
}

App.js

  • App.js에서는 url을 어떤값을 입력했을때 어떤 페이지로 이동할지에 대한 내용을 작성한다.
  • 여기서 사용되는게 react-router-dom이다.
  • 참고로 function App()으로 정의 되어 있었는데, epoxrt default function App()으로 변경하면 아래 export를 따로 해줄필요가 없다. (매번 자동적으로 입력했는데 이게 좋은듯)
  • 이렇게 가져온 isAuthenticated의 값은 global 하게 사용이 가능하다
  • PrivateRoute, PublicRoute를 나누어서 로그인한 사용자, 로그인하지 않은 사용자의 Route를 나눈다.
  • Context API는 리액트 프로젝트에서 전역적으로 사용할 데이터가 있을 때 유용한 기능
  • 사용 예
    • 로그인 정보
    • 애프리케이션 환경 설정
    • 테마
    • redux, react-router, styled-components 의 라이브러리
  • 컴포넌트 간에 데이터를 props로 저달하기 때문에 여기저기서 필요한 데이터가 있을 때는 주로 최상위 컴포넌트인 App의 state에 넣어서 관리
  • 하지만 props으로 아주 ~ 먼 컴포넌트에 전달하려면 여러개의 컴포넌트를 지나가야 하니... 유지 보수성이 낮아질 가능성이 있음
  • 위 문제점을 해결하기 위해서 redux나 MobX 같은 상태 관리 라이브러리를 사용하면 전역 상태 관리를 더 편하게 처리
  • 리액트 v16.3 업데이트 이후에는 Context API가 많이 개선되었기 때문에 별도의 라이브러리 사용할 필요 없음
import { createContext } from 'react';

const ColorContext = createContext({ color: 'black' });

export default ColorContext; 
import ColorBox from './components/ColorBox';
<ColorBox></ColorBox>

Provider 이용하기

  • Provider를 사용하면 Context의 값을 변경할 수 있다.
  • 아래와 같이 컴포넌트 ColorBoxColorContext.Provider를 통해 color 값을 변경할 수 있다.
  • Provider를 사용할때는 value 값을 명시해줘야 제대로 작동
      <ColorContext.Provider value={{ color: 'red' }}>
        <div>
          <ColorBox></ColorBox>
        </div>
      </ColorContext.Provider>

동적으로 사용하기

  • context의 value에는 상태값, 함수를 전달할 수 있다.
  • 아래와 같이 state, actions을 따로 정의한 ColorContext를 정의
  • ColorProvider 를 정의
import React, { createContext, useState } from 'react';

// 객체에서 state, actions을 분리해서 정의하면 나중에 사용하기가 편함
const ColorContext = createContext({
  state: { color: 'black', subcolor: 'red' },
  actions: {
    setColor: () => {},
    setSubcolor: () => {}
  }
});

const ColorProvider = ({ children }) => {
  const [color, setColor] = useState('black');
  const [subcolor, setSubcolor] = useState('red');

  const value = {
    state: { color, subcolor },
    actions: { setColor, setSubcolor }
  };

  return (
    <ColorContext.Provider value={value}>{children}</ColorContext.Provider>
  );
};

const { Consumer: ColorConsumer } = ColorContext;
export { ColorProvider, ColorConsumer };
export default ColorContext;
import React from 'react';
import { ColorConsumer } from '../contexts/color';

const ColorBox = () => {
  return (
    <ColorConsumer>
      {(
        { state } // 객체 비구조화 할당 문법
      ) => (
        <>
          <div
            style={{
              width: '64px',
              height: '64px',
              background: state.color
            }}
          />
          <div
            style={{
              width: '32px',
              height: '32px',
              background: state.subcolor
            }}
          />
        </>
      )}
    </ColorConsumer>
  );
};

export default ColorBox;

사용하는 쪽에서는 아래와 같이

      <ColorProvider>
        <div>
          <ColorBox></ColorBox>
        </div>
      </ColorProvider>

색상 선택하는 컴포넌트 생성

  • Context에 들어있는 actions에 넣어준 함수를 호출하는 컴포넌트 생성
# SelectColor.js
import React from 'react';
import { ColorConsumer } from '../contexts/color';

const colors = [
  'red',
  'oragne',
  'yellow',
  'green',
  'blue',
  'indigo',
  'violoet'
];

const SelectColors = () => {
  return (
    <div>
      <h2>색상을 선택하세요.</h2>
      <ColorConsumer>
        {({ actions }) => (
          <div style={{ display: 'flex' }}>
            {colors.map(color => (
              <div
                key={color}
                style={{
                  background: color,
                  width: '24px',
                  height: '24px',
                  cursor: 'pointer'
                }}
                onClick={() => actions.setColor(color)}
                onContextMenu={e => {
                  e.preventDefault(); // 기존 오른쪽 메뉴가 뜨는 것을 방지
                  actions.setSubcolor(color);
                }}
              />
            ))}
          </div>
        )}
      </ColorConsumer>
      <hr />
    </div>
  );
};

export default SelectColors;
  • App.js에서는 SelectColors의 컴포넌트를 추가
      <ColorProvider>
        <div>
          <SelectColors></SelectColors>
          <ColorBox></ColorBox>
        </div>
      </ColorProvider>

useContext Hook 사용

  • 리액트 내장되어 있는 Hooks 중에서 useContext를 사용해보자
  • useContext Hook은 함수형 컴포넌트에서만 사용할 수 있다.
  • ColorBox를 아래와 같이 수정
import React, { useContext } from 'react';
import { ColorContext } from '../contexts/color';

const ColorBox = () => {
  const { state } = useContext(ColorContext);
  return (
    <>
      <div
        style={{
          width: '64px',
          height: '64px',
          background: state.color
        }}
      />
      <div
        style={{
          width: '32px',
          height: '32px',
          background: state.subcolor
        }}
      />
    </>
  );
};

export default ColorBox;

static contextType

  • 클래스형 컴포넌트에서 Context를 좀 더 쉽게 사용하기 위해서 static contextType을 정의
  • 기존에 SelectColors의 컴포넌트를 클래스형으로 변경
  • static contextType을 정의하면 클래스 메소드에서도 Context에 있는 함수 호출이 가능
  • 단점이라면 한 클래스에서 하나의 context만 사용할 수 있음
  • useContext를 사용하는게 더 권장됨
  • 아래가 기존
import React from 'react';
import { ColorConsumer } from '../contexts/color';

const colors = [
  'red',
  'oragne',
  'yellow',
  'green',
  'blue',
  'indigo',
  'violoet'
];

const SelectColors = () => {
  return (
    <div>
      <h2>색상을 선택하세요.</h2>
      <ColorConsumer>
        {({ actions }) => (
          <div style={{ display: 'flex' }}>
            {colors.map(color => (
              <div
                key={color}
                style={{
                  background: color,
                  width: '24px',
                  height: '24px',
                  cursor: 'pointer'
                }}
                onClick={() => actions.setColor(color)}
                onContextMenu={e => {
                  e.preventDefault(); // 기존 오른쪽 메뉴가 뜨는 것을 방지
                  actions.setSubcolor(color);
                }}
              />
            ))}
          </div>
        )}
      </ColorConsumer>
      <hr />
    </div>
  );
};

export default SelectColors;
  • 이후가 클래스 컴포넌트로 변경 후
import React, { Component } from 'react';
import ColorContext from '../contexts/color';

const colors = [
  'red',
  'oragne',
  'yellow',
  'green',
  'blue',
  'indigo',
  'violoet'
];

class SelectColors extends Component {
  static contextType = ColorContext;

  handleSetColor = color => {
    this.context.actions.setColor(color);
  };

  handleSetSubcolor = subcolor => {
    this.context.actions.setSubcolor(subcolor);
  };

  render() {
    return (
      <div>
        <h2>색상을 선택하세요.</h2>
        <div style={{ display: 'flex' }}>
          {colors.map(color => (
            <div
              key={color}
              style={{
                background: color,
                width: '24px',
                height: '24px',
                cursor: 'pointer'
              }}
              onClick={() => this.handleSetColor(color)}
              onContextMenu={e => {
                e.preventDefault(); // 기존 오른쪽 메뉴가 뜨는 것을 방지
                this.handleSetSubcolor(color);
              }}
            />
          ))}
        </div>
        <hr />
      </div>
    );
  }
}

export default SelectColors; 

+ Recent posts