[TIL] 2022-07-01 / 75일차 (Context API)

미음제

·

2022. 7. 1. 20:11

 

 

중간 프로젝트가 끝나고 React 심화 수업을 수강함과 동시에 과제가 시작되었다. 과제는 강사님 회사에서 진행하는 과제로 재활용할 수 있는 컴포넌트를 만드는 연습을 하기 위해 내준 것 같다. 강의 내용이 어려워서 강의보다는 얼른 과제를 끝내고 싶은 마음에 빠르게 과제부터 진행했다. 어느 정도 완성은 했지만 재활용할 수 있는 컴포넌트를 만들었냐고 물어보면 아니라고 말할 것 같다. 아직까진 추상화 개념이 어렵다. 많은 연습이 필요할 것 같다.

 

과제를 진행하면서 Context API를 도입하고자 했다. 사실 이 과제에서 Context API가 필요하냐 하면 그렇지 않다. 큰 프로젝트도 아니고 간단한 컴포넌트라서 불필요하다. 중간 팀 프로젝트를 진행할 때 User 객체를 전역적으로 관리하며 Context API를 사용했었는데, 내가 Context API를 다뤄보지 않았고 다른 팀원 분이 작성해준 Context를 그대로 가져다 써서 연습 삼아 Context API를 도입하고자 했다.

 

Context API 강의를 처음 수강했을 때 '편리하다'는 느낌만 받고 제대로 학습하지 못했는데, 앞으로 Context API를 사용하거나 Redux, Recoil을 사용하게 될 때 Conext API 개념이 정리되어 있어야 할 것 같아서 새로 정리하고자 한다.

 

Context API

 

리액트로 애플리케이션을 만들면 여러개의 하위 컴포넌트들로 구성된 모습이 된다. 최상위 App 컴포넌트에서 트리 형태로 하위 컴포넌트가 뻗어나간다. 리액트의 데이터 흐름은 위에서 아래로, 즉 부모 컴포넌트에서 자식 컴포넌트로 흐르는 구조이다. 

 

 

데이터는 props로 자식 컴포넌트로 전달이 된다. 위 사진처럼 컴포넌트 구조가 단순하다면 문제가 되지 않지만 규모가 큰 프로젝트라면 하위 컴포넌트는 무수히 많고 최상위에서 최하단 컴포넌트까지 props를 전달하려면 수많은 컴포넌트를 거쳐가야 한다.

 

리액트는 이런 문제를 해결하기 위해 Context API를 제공한다. Context는 애플리케이션 안에서 전역적으로 사용되는 데이터들을 쉽게 공유할 수 있는 방법을 제공해준다.

 

 

전역적인 데이터를 Context로 전달하게 되면 prop drilling 현상을 줄일 수 있다. prop drilling 현상은 하위 컴포넌트에서 필요한 props을 전달하기 위해 사용하지 않는 컴포넌트를 거쳐 내려가는 현상을 말한다. 

 

Context를 사용해 데이터를 공유하면 데이터를 필요로 하는 컴포넌트에서 useContext라는 hook을 통해 해당 데이터에 접근할 수 있게 된다.

 

Context를 사용하면 prop을 사용할 필요가 없다?

 

prop drilling 현상을 줄일 수 있고, 원하는 곳에서 쉽게 사용할 수 있기 때문에 prop을 사용해야 하는 의문이 들 수 있다. 그러나 다음과 같은 이유에서 Context는 적절하게 사용해야한다(너무 남발하면 안 된다).

 

  • 1. Context를 무분별하게 사용하면 컴포넌트 재사용성이 떨어진다.
  • 2. prop drilling 현상을 피하기 위해서 Context를 사용한다면, Component Composition(컴포넌트 합성)을 우선적으로 고려한다.

컴포넌트가 Context에 의존하게 되면 컴포넌트 재사용성이 떨어지게 된다. Context에 의존하게 되면 내려주는 데이터가 없는 경우가 발생할 수 있어, Context의 데이터에 의존성이 생기게 된다.

 

컴포넌트 간 거리가 멀지 않은 경우(내가 생각해낸 개념이다) Context를 사용하는 것은 비효율적이다. 오히려 해당 컴포넌트는 분리된 형태보다는 합쳐진 형태가 맞다고 생각할 수 있다.

 

Context API는 전역 상태 관리 도구?

 

딱 잘라 얘기를 하자면 Context API는 상태 관리를 하는 척하는 녀석이다. Redux와 같은 라이브러리 대체재로 사용되어 전역 상태 관리 도구라고 이해하기 쉽지만 그렇지 않다.

 

 

 

Context – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있습니다.

출처 : 리액트 공식 문서


Context API의 목적은 props을 전달하는 데 있다. 단순 props을 전달하기 위한 녀석이지 관리를 하는 녀석이라고 볼 순 없다. Context 컴포넌트 내부에서 useState, useReducer와 같은 hook을 이용해 state를 받고, 수정하기도 해서 상태 관리를 하는 것 같지만, 상태 관리를 흉내 내는 것이라 볼 수 있다.

 

한마디로 정리하자면 Context API는 prop drilling 현상을 피하고, props을 전달하기 위한 도구라고 생각하면 된다.

 

Context API 사용법

 

우선 Context API를 사용하려면 Context를 만들어야 한다. 리액트의 createContext를 통해 새로운 Context를 만들 수 있다. createContext()의 인자로 넘겨준 값이 해당 Cotext의 default value가 된다.

 

src 디렉터리 내부에 context 디렉터리를 생성하고, Context를 생성하기 위한 파일을 생성한다.

 

import { createContext } from "react";

export const myContext = createContext();

 

myContext라는 이름으로 Context를 생성했고, 인자 값으로 아무것도 넘겨주고 있지 않아 default value는 없는 상태이다.

 

생성한 Context의 값을 원하는 컴포넌트에서 가져오기 위해서는 컴포넌트 내부에서 useContext hook을 사용하면 된다.

 

// src/hooks/useValueContext.js

import { useContext } from "react";
import { myContext } from "src/context/ContextProvider";

const useValueContext = () => useContext(myContext);

export default useValueContext;

 

hooks 디렉터리 내부에 커스텀 hook을 통해 useContext를 사용할 수 있도록 정의했다. useValueContext라는 이름으로 방금 생성한 myContext의 데이터를 가져오기 위해 useContext의 인자로 myContext를 넘겼다.

 

이렇게 생성한 커스텀 hook을 원하는 컴포넌트에서 import 하여 Context의 값을 전달받을 수 있다.

 


 

Context의 default value가 아닌 value를 넘기고 싶을 땐 원하는 컴포넌트를 Context.Provider로 감싸주면 된다. Context.Provider로 감싸진 하위 컴포넌트에서 value를 사용하려면 똑같이 useContext hook을 사용한다. 이때, Conext.Provider 컴포넌트의 value 값으로 아무것도 넘기지 않으면 제대로 동작하지 않는다. value 값을 넘겨야만 제대로 사용할 수 있다.

 

return (
    <myContext.Provider value={value}>
    	<컴포넌트 1 />
        <컴포넌트 2 />
    </myContext.Provider>
);

 

이런 식으로 컴포넌트를 감싸고 value값으로 Context 내부의 state 값을 넘기게 되면 하위 컴포넌트에서 해당 값을 사용할 수 있게 된다.

 


 

이번 과제에서 Conext를 사용한 구조는 다음과 같다.

 

  • 1. Form 컴포넌트에서 생성된 데이터를 Context에 저장한다.
  • 2. 결과를 출력할 ResultBox 컴포넌트에서 가공된 데이터를 가져와 결과로 보여준다.
  • 3. 결과가 있고, 결과가 변한 경우에만 해당 데이터를 렌더링 한다.

 

import { createContext, useMemo, useState } from "react";

export const valueContext = createContext();
export const actionContext = createContext();

function ContextProvider({ children }) {
  const [data, setData] = useState("");

  const actions = useMemo(
    () => ({
      updateData(data) {
        setData(data);
      },
    }),
    []
  );
  return (
    <actionContext.Provider value={actions}>
      <valueContext.Provider value={data}>{children}</valueContext.Provider>
    </actionContext.Provider>
  );
}

export default ContextProvider;

 

우선 value를 전달할 valueContext와 Context의 value를 수정하기 위한 actionContext로 Context를 분리했다. Context 컴포넌트 내부에는 Form 데이터에서 전달받은 값을 로컬 state(data)로 저장한다. Form 컴포넌트에서 Submit 이벤트가 발생하면 새로운 데이터를 setData(data)를 통해 state를 업데이트해준다. 해당 데이터를 사용하는 ResultBox 컴포넌트에서는 이 Context의 data state를 가져다 쓰는 구조이다.

 

ContextProvider 컴포넌트는 상위에 actionContext 하위에 valueContext로 구성되어 있다.

 

import { useContext } from "react";
import { actionContext } from "src/context/ContextProvider";

const useActionContext = () => useContext(actionContext);

export default useActionContext;

 

import { useContext } from "react";
import { valueContext } from "src/context/ContextProvider";

const useValueContext = () => useContext(valueContext);

export default useValueContext;

 

value와 action을 사용하기 위한 useContext는 커스텀 hook으로 새로 정의했다. value를 원하는 곳에서는 useValueContext hook을 import 해서 사용하고, value를 수정하기 위한 action을 원하는 곳에서는 useActionContext hook을 import 해서 사용한다.

 

// Form 컴포넌트

// 생략

function Form({ action }) {
  const [type, setTypes] = useState("");
  const [inputValue, typeValue, onSubmit] = useOnSubmit("", type);
  const inputRef = useRef(null);
  const { updateData } = useActionContext();
  // 생략

  useEffect(() => {
    const data = dataFormat(typeValue, inputValue);
    updateData(data);
  }, [typeValue, inputValue, dataFormat, updateData]);

  useEffect(() => {
    inputRef.current.value = "";
  }, [type]);

  return (
    <FormWrapper>
      <StyledForm action={action} onSubmit={onSubmit}>
        // 생략
      </StyledForm>
    </FormWrapper>
  );
}

export default Form;

 

onSubmit 이벤트가 발생해 onSubmit 함수가 실행되면, 지역 state 값인 inputValue, typeValue가 바뀌게 된다. 해당 값을 통해 dataFormat() 함수로 새로운 데이터를 생성하고, 생성된 데이터는 const { updateData } = useActionContext()로 가져온 updateData action을 활용해 Context의 지역 state를 업데이트한다.

 

// ResultBox 컴포넌트

// 생략

function ResultBox() {
  const data = useValueContext();
  return (
    <>
      {data && (
        <ResultBoxWrapper>
          // 생략
          <ResultContent fontSize="32px" color="#5d5d5d" bold>
            {data}
          </ResultContent>
        </ResultBoxWrapper>
      )}
    </>
  );
}

export default ResultBox;
 

Context의 로컬 state를 const data = useValueContext()로 가져온다. 해당 data가 있는 경우 jsx를 return 하도록 되어 있다.

 

// App

import Form from "@components/Form";
import Header from "@components/Header";
import styled from "@emotion/styled";
import ContextProvider from "./context/ContextProvider";
import ResultBox from "@components/ResultBox";

const AppContainer = styled.div`
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
`;

function App() {
  return (
    <ContextProvider>
      <AppContainer>
        <Header type={1} color="#5d5d5d" bold>
          Input Formatting
        </Header>
        <Form />
      </AppContainer>
      <ResultBox />
    </ContextProvider>
  );
}

export default App;

 

최상위 App에서 ContextProvider로 하위 컴포넌트들을 감싸주었다. 하위 컴포넌트로 From 컴포넌트와 ResultBox 컴포넌트가 있다. Form 컴포넌트에서 생성된 데이터를 ResultBox 컴포넌트에서 렌더링 하기 위한 데이터로 사용하는 구조이다.

 


 

오늘의 회고

 

기본 설계를 좀 더 잘했다면 컴포넌트가 많지 않기 때문에 ContextAPI를 사용하지 않고 구성했어도 충분했을 과제이다.  컴포넌트도 재활용이 될 만큼 구성한 것 같지 않고, 컴포넌트 분리 및 합성이 아직 어렵다. Context API를 사용해보고자 도입했는데, 정확하게 사용된 것인지는 모르겠다. 일요일에 PR을 제출하고 다음 주부터 코드 리뷰를 받게 되는데 리뷰를 받으면서 잘못 사용되고 있는 점에 대해 피드백받고 Context API에 대한 개념을 더 업데이트할 계획이다.

 

추가로 Form 내부의 Radio button의 변경에 따라 state값이 변경되고 state 변경에 따라 Input의 value를 초기화하고 싶어 useRef를 사용했는데 current 객체가 계속 undefined가 떠서 애를 먹었다. 컴포넌트 내부 jsx에서 HTML 자체를 사용하면 useRef를 사용해도 괜찮은데, 해당 HTML을 컴포넌트로 분리하면 useRef를 바로 사용하면 안 된다는 문제였다. 이렇게 분리된 하위 컴포넌트에 useRef를 사용하고자 하면 fowardRef를 사용해야 한다. 이 내용도 다시 정리해서 글을 작성할 계획이다.

 

Context API를 조금 더 공부하고, Redux, Redux toolkit도 정리하고 hook, 커스텀 hook도 더 공부해야 할 것 같다. 중간 프로젝트 땐 리액트가 처음이라 어려웠다면 최종 프로젝트 땐 리액트를 한 번 경험해봤기 때문에 조금 더 잘해보고 싶다. 중간 프로젝트의 아쉬움과 최종 프로젝트를 잘 마치고 싶기 때문에 많이 공부해야 할 것 같다. 리액트가 자유도가 높은 만큼 많은 내용을 공부해야 할 것 같다.

 

 

반응형