[TIL] 241120_React: Context에 대하여

지코·2024년 11월 20일
0

Today I Learned

목록 보기
55/66
post-thumbnail

⚡️ Context에 대하여

일 년 전쯤 context에 대해 간단하게 살펴본 적 있다. 하지만 그 이후로 context를 제대로 사용해본 적이 없어 순식간에 잊혀진 ...!
🔗 참고: React-DOM과 Context-이해하기
리액트 강의를 들으며 다시 찾아보고, 고민해보고, 꼼꼼히 기록해보려고 한다.

Context'인접한 컴포넌트 간에만 인자를 전달할 수 있는 구조적 한계' 를 해결하는, 중간 컴포넌트 없이 데이터를 비교적 직접 전달할 수 있는 방법이다.

  • Provider: 메시지 공급자이며, 리액트 렌더 사이클과 연동하기 위해 상태를 가진다.
  • Consumer: Provider가 가지고 있는 상태를 구독할 수 있는 컴포넌트로, 이 값이 render() 함수의 props 인자로 들어온다.
  • Provider 컴포넌트 하위에 Consumer 컴포넌트를 사용하기만 하면, 멀리 있는 컴포넌트의 메시지를 구독할 수 있는 방식이다.

⚡️ createContext() 직접 구현해보기

📁 EventEmitter.js

const createEventEmitter = (value) => {
  let handlers = [];

  const on = (handler) => handlers.push(handler);
  const off = (handler) => {
    handlers = handlers.filter((h) => h !== handler);
  };

  const get = () => value;  // closer
  const set = (newValue) => {
    value = newValue;
    handlers.forEach(handler => handler(value));
  }

  return { on, off, get, set };
};

export default createEventEmitter;

먼저 자바스크립트로 위와 같은 이벤트 발행자 createEventEmitter 를 구현하였다. 이 함수는 이벤트를 구독(on), 해지(off), 값을 가져오고(get), 변경하는(set) 작업을 하기 위해 구현되었으며, 후에 구현할 createContext 함수에서 사용될 예정이다.

📁 MyReact.js

import React from "react";
import createEventEmitter from "shared/lib/EventEmitter";
// context를 만드는 createContext 함수를 제공하는 모듈
const MyReact = (function () {
  function createContext(initialValue) {
    const emitter = createEventEmitter(initialValue);
    // value 값이 변경될 때 Consumer에게 알림
    class Provider extends React.Component {
      componentDidMount() {
        emitter.set(this.props.value);
      }

      componentDidUpdate() {
        emitter.set(this.props.value);
      }

      render() {
        return <>{this.props.children}</>;
      }
    }
    // emitter가 전달한 값을 소비하는 역할
    class Consumer extends React.Component {
      constructor(props) {
        super(props);

        this.state = {
          value: emitter.get(),
        };
        this.setValue = this.setValue.bind(this); // 비동기 작업이므로 this 바인딩
      }

      setValue(nextValue) {
        this.setState({ value: nextValue });
      }

      componentDidMount() {
        // 이벤트 에미터의 구독 함수로 전달
        emitter.on(this.setValue);
      }

      componentWillUnmount() {
        // 이벤트 에미터의 구독 해지
        emitter.off(this.setValue);
      }

      render() {
        return <>{this.props.children(this.state.value)}</>;
      }
    }
    // context = Provider와 Consumer로 이루어진 객체
    return { Provider, Consumer };
  }

  return { createContext };
})();

export default MyReact;

실제로 React에서 사용하는 createContext 함수를 자바스크립트를 이용해, MyReact라는 객체로 감싸 구현하였다.

1️⃣ Provider
Provider는 createContext 함수로 생성된 context의 값을 정의하고, 해당 값을 하위 트리(Consumer)로 전달하는 역할을 수행한다.

componentDidMount(), componentDidUpdate() 함수에서 Provider 컴포넌트의 value로 받은 값을 emitter.set 함수를 통해 상태로 관리하고, 상태가 바뀌면 연결된 모든 Consumer 컴포넌트들에게 알려준다. 그리고 children을 렌더링한다.

2️⃣ Consumer
Consumer는 Provider에서 전달한 값을 구독하고, 이를 활용하는 역할을 한다.
Provider가 context의 값을 가지고 있기 때문에, Consumer가 Provider와 얼마나 멀리 떨어져 있는지는 중요하지 않다‼️

Provider에서 전달된 값을 가져와 상태(state)로 관리하고, componentDidMount() 함수에서 emitter.on() 함수를 통해 상태 변경 이벤트를 구독하는데, 이 이벤트는 Provider에서 emitter.set() 함수를 호출할 때 발생한다.
Provider의 emitter.set() 함수 호출 시 Consumer의 setValue() 함수가 호출되며 value 값이 업데이트되는데, 이 때 컴포넌트가 리렌더링된다.

다시 정리하면, state가 업데이트되면 이를 다시 렌더링하여 최신 데이터를 제공하는 역할을 한다는 뜻이다.

➡️ createContext 함수는 이러한 Provider와 Consumer로 이루어진 객체이며, MyReact.createContext(초기값) 과 같은 형태로 사용할 수 있다.

그럼 이제 활용을 해보자!

📁 App.jsx

// MyReact 활용 예시 코드
import React from "react";
import MyReact from "./lib/MyReact";

const countContext = MyReact.createContext({
  count: 0,
  setCount: () => {},
});
// provider
class CountProvider extends React.Component {
  constructor(props) {
    super(props);

    this.state = { count: 0 };
  }

  render() {
    const value = {
      count: this.state.count,
      setCount: (nextValue) => this.setState({ count: nextValue }),
    };

    return (
      <countContext.Provider value={value}>
        {this.props.children}
      </countContext.Provider>
    );
  }
}
// consumer
const Count = () => {
  return (
    <countContext.Consumer>
      {(value) => <div>{value.count}</div>}
    </countContext.Consumer>
  );
};

const PlusButton = () => {
  return (
    <countContext.Consumer>
      {(value) => (
        <button onClick={() => value.setCount(value.count + 1)}>
          + 카운트 올리기
        </button>
      )}
    </countContext.Consumer>
  );
};

export default () => (
  <CountProvider>
    <Count />
    <PlusButton />
  </CountProvider>
);

먼저 MyReact.createContext 함수를 사용해 context 객체인 countContext를 생성한다. 인자로 넣은 객체는 초기값이며, count 값과 count 값을 업데이트하기 위한 setCount 함수를 가지고 있다.

CountProvider 컴포넌트는 count 변수의 상태를 관리하며, countContext.Provider를 통해 값을 하위 트리에 전달한다.
이 때 전달하는 값은 현재 state.count 값으로 업데이트된 count와, count 값을 업데이트하는 setCount 함수로 이루어진 객체이다.

전달 받은 값을 사용하기 위한 Consumer 컴포넌트로 Count 컴포넌트와 PlusButton 컴포넌트가 있다.
먼저 Count 컴포넌트는 countContext.Consumer를 통해 받아온 value 값을 div 객체를 통해 화면에 렌더링한다.
그리고 PlusButton 컴포넌트는 버튼 클릭 시 countContext.Consumer를 통해 받아온 value 값을 1씩 증가시킨다. 이 때 setCount 함수를 사용한다.

최종적으로 App.jsx 파일은 Consumer를 담당하는 CountPlusButton 컴포넌트를 Provider인 CountProvider 컴포넌트로 감싸 반환한다.

⚡️ 상태의 흐름 정리하기

  1. + 카운트 올리기 버튼을 클릭한다.
() => value.setCount(value.count + 1)

위 함수가 호출되며, CountProvider 컴포넌트의 setState() 함수가 호출되어 count 값이 업데이트된다.
2. this.state.count 의 값이 업데이트된다.
3. countContext.Provider의 value 값이 업데이트되므로, Consumer들에게 새로운 값을 전달한다.
4. CountPlusButton 컴포넌트가 새롭게 렌더링된다.

Reference

👩🏻‍🏫 [리액트 2부] 고급 주제와 훅
https://www.inflearn.com/course/리액트-고급주제와-훅-2부

📄 React 공식 문서: Context-React
https://ko.legacy.reactjs.org/docs/context.html#before-you-use-context

profile
꾸준하고 성실하게, FE 개발자 역량을 키워보자 !

0개의 댓글