본문 바로가기
Web/React.js

[STUDY] 컨텍스트

by 폴우정킴 2021. 3. 13.

Context

Context를 이용하면 component 마다 props로 데이터를 넘겨주지 않고도 component 트리 전체에 데이터를 전달 혹은 받을 수 있습니다.

 

* Context를 통해서 데이터를 전역으로 다룰 수 있지만 비슷한 유형의 Redux 나 Mobx가 실무에서 많이 쓰이고 있습니다. 공식 문서에서도 규모가 큰 프로젝트에서는 사용하지 않는 것을 권장하고 있습니다.

 

Context를 사용해서 UI테마나 location에 따른 언어 설정 같은 정보를 components간 props로 전달 하는 것이 아니라 공유하게 할 수 있습니다.

 

 

 

위의 예시 처럼 context를 사용하면 중간에 있는 엘리먼트들에게 props를 넘겨주지 않고 공유하고 있는 전역객체인 context에서 받아서 사용할 수 있습니다. ( 원한는 값을 컴포넌트 트리 깊숙한 곳까지 보낼 수 있습니다.)

 

하지만 이렇게 여러 레벨에 걸쳐 props를 넘길때 context 보다 컴포넌트 합성이 더 합리적일 수도 있습니다.

<Page user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

 

가장 최 하위에 있는 Avatar component에서 avatarSize 데이터가 필요 할 뿐인데 props를 여러 단계에 걸쳐 내보내고 있습니다. 이때 중간 레벨에 어떠한 컴포넌트가 추가 된다면 거기에서 또한 props를 받아서 내보줘야 합니다.

 

하지만 Avatar 컴포넌트 자체를 넘져주는 컴포넌트 합성 방식으로 중간에서 props를 넘기는 처리를 하지 않고 최 하위 컴포넌트에서 받을 수 있습니다.

// 합성 컴포넌트
function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// 이제 이렇게 쓸 수 있습니다.
<Page user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<PageLayout userLink={...} />
// ... 그 아래에 ...
<NavigationBar userLink={...} />
// ... 그 아래에 ...
{props.userLink}

 

이러한 제어의 역전(inversion of control)를 이용하여 넘기는 props의 수를 줄이고 최상위 컴포넌트의 제어력을 더 키움으로 깔끔한 코드를 작성 할 수 있습니다. 하지만 이 방법이 언제나 옳지는 않습니다. 상위 컴포넌트가 난해해질 수 있습니다.

// 슬록 방식
function Page(props) {
  const user = props.user;
  const content = <Feed user={user} />;
  const topBar = (
    <NavigationBar>
      <Link href={user.permalink}>
        <Avatar user={user} size={props.avatarSize} />
      </Link>
    </NavigationBar>
  );
  return (
    <PageLayout
      topBar={topBar}
      content={content}
    />
  );
}

슬롯 패턴은 부모 컴포넌트와 자식 컴포넌트를 한번에 직관적으로 볼 수 있다는 장점이 있습니다. 하지만 자식 컴포넌트가 많을 수록 이 방식으로 관리하기가 힘들어 집니다.

API

데이터 값이 변할 때마다 모든 하위 컴포넌트에게 알려주는 것이 context 입니다. 흔하게 유저의 location을 알려주는 로케일, 테마, 데이터 캐시 등을 편리하게 관리 할 수 있습니다.

const MyContext = React.createContext(defaultValue)

const MyContext = React.createContext(defaultValue)

이렇게 context 객체를 만듭니다. 이 context 객체를 '구독' 하고 있는 컴포넌트를 렌더링 할 때 React는 트리 상위에서 가장 가까이 있는 그리고 짝이 맞는 Provider 로 부터 데이터를 읽습니다.

 

defaultValue 매개변수는 트리에서 적잘한 Provider를 찾지 못했을때 쓰이는 값입니다. ( 컴포넌트를 독립적으로 테스트할 때 유용함)

 

<MyContext.Provider value= { /* 어떤 값 */ }

<MyContext.Provider value= { /* 어떤 값 */ }

Provider ( Context 오브젝트에 포함되어 있어서 .으로 불러오는 것) 는 context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 합니다.

 

value에 받은 값은 하위에 있는 컴포넌트에게 전달합니다. 값을 전달받을 수 있는 컴포넌트의 수는 제한이 없습니다.

 

Provider하위에서 context를 구독하는 모든 컴포넌트들은 이 value의 값이 바뀔 때마다 다시 렌더링 됩니다.

<MyContext.Consumer>
	{ value => /* context 값을 이용한 레더링 */ }
<MyContext.Consumer>

 

이 컴포넌트는 context 변화를 구독하는 컴포넌트입니다. 함수형 컴포넌트 안에서 context를 읽기 위해 쓸 수 있습니다. 이 함수가 받는 value 매개변수 값은 해당 context의 Provider 중 상위 트리에서 가장 가까운 Provider의 value prop과 동일합니다. 상위에 Provider가 없다면 value 매개변수 값은 createContext()에 보냈던 defaultValue와 동일할 것입니다.

Context.displayName

React 개발자 도구에서 context는 이 문자열로 표시 됩니다.

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools

 

예시 코드

video clip 을 context api로 관리하기

import React from 'react';

const videoStyles = {
  marginTop: '100px',
  width: '50vw',
};

const VideoClip = () => (
  <video style={videoStyles} controls>
    <source
      src="https://react-context.s3.eu-central-1.amazonaws.com/Pouring+Of+Milk.mp4"
      type="video/mp4"
    />
  </video>
);

export default VideoClip;

src/components/video-clip.component.js

 

import React from 'react';

const styles = {
  width: '100px',
  height: '5vh',
  backgroundColor: 'black',
  color: 'white',
  fontSize: '20px',
  marginTop: '20px',
};

const PlayPauseButton = () => <button style={styles}>Click</button>;

export default PlayPauseButton;

src/components/play-pause-button.component.js

 

 

import React from 'react';
import PlayPauseButton from './play-pause-button.component';

const Controls = () => <PlayPauseButton />;

export default Controls;

src/components/controls.component.js

 

import React from 'react';
import VideoClip from './components/video-clip.component';
import Controls from './components/controls.component';
import './App.css';

function App() {
  return (
    <div className="App">
        <VideoClip />
        <Controls />
    </div>
  );
}

export default App;

src/App.js

 

import React, { createContext } from 'react';

const VideoContext = createContext({
  status: 'paused',
  togglePlayPause: () => {},
});

export default VideoContext;

src/context/video.context.js

 

import React, { useState} from 'react';

...

function App() {
  const [status, setStatus] = useState('paused');
  const togglePlayPause = () => setStatus(status === 'playing' ? 'paused' : 'playing');
...
}

src/App.js

 

...
import VideoContext from './context/video.context';
...

return (
    <div className="App">
      <VideoContext.Provider
        value={{
          status,
          togglePlayPause,
        }}
      >
        <VideoClip />
        <Controls />
      </VideoContext.Provider>
    </div>
  );
...

 

src/App.js

 

import React, { useContext } from 'react';
import VideoContext from '../context/video.context';
...

const PlayPauseButton = () => {
  const { status, togglePlayPause } = useContext(VideoContext);
  return (
    <button style={styles} onClick={togglePlayPause}>
      {status === 'playing' ? 'PAUSE' : 'PLAY'}
    </button>
  );
};

...

src/components/play-pause-button.component.js

 

import React, { useContext, useEffect, createRef } from 'react';
import VideoContext from '../context/video.context';

...

const VideoClip = () => {
  const { status } = useContext(VideoContext);

  const vidRef = createRef();

  useEffect(() => {
    if (status === 'playing') {
      vidRef.current.play();
    } else if (status === 'paused') {
      vidRef.current.pause();
    }
  });

  return (
    <video style={videoStyles} controls ref={vidRef}>
      <source
        src="https://react-context.s3.eu-central-1.amazonaws.com/Pouring+Of+Milk.mp4"
        type="video/mp4"
      />
    </video>
  );
};

src/components/video-clip.component.js

'Web > React.js' 카테고리의 다른 글

[STUDY] 고차 컴포넌트  (0) 2021.03.16
[STUDY] Ref 전달하기  (0) 2021.03.15
[STUDY] 에러 경계  (0) 2021.03.12
[STUDY] 코드분할  (0) 2021.03.10
[STUDY] 접근성  (0) 2021.03.09

댓글