⚛️ 내가 보려고 만든 React Context API 정리 ✨

혜혜·2023년 10월 26일
0

React

목록 보기
7/9
post-thumbnail

이번 프로젝트에서 Context api를 통해 상태 관리를 진행해 보기로 해서 Context api에 대해 공부한 글을 정리해 보려고 한다. 지금까지 Context api도 제대로 사용해 본 적 없으면서 편한 상태 관리 도구들을 덥썩 썼던 게 후회가 된다... React의 많은 상태 관리 도구가 Context api를 기반으로 동작한다고 하니, Context api를 공부해 보는 게 상태관리의 전반적인 개념을 이해하는 데 많은 도움이 될 수 있을 것 같다.

✨ Context 란?

💡 React에서 단계마다 일일이 props를 넘겨주지 않고도, 컴포넌트 트리 전체에 데이터를 제공할 수 있게끔 하는 도구를 말한다.

  • 일반적인 React 애플리케이션에서, 데이터는 위에서 아래로(즉, 부모 → 자식) props를 통해 전달됨
  • But, 애플리케이션 안의 여러 컴포넌트에 전해줘야 하는 props의 경우, 이 과정이 번거로울 수 있다!
    → Context를 이용하면 이 문제를 해결할 수 있음

✨ 언제 Context를 써야 할까

  • Context는 React 컴포넌트 트리 안에서 전역적(global)인 데이터를 공유할 수 있도록 고안된 방법
  • ex) 현재 로그인한 유저, 테마, 선호하는 언어...
class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}
  • AppToolbarThemedButton
  • 사실 theme 색깔이 필요한 컴포넌트는 ThemedButton인데,
    Toolbar는 오로지 ThemedButton에게 theme을 전달해 주기만을 위해 App에게서 theme을 받음
  • 즉, Toolbar 스스로는 theme을 사용 안 할 거고 필요없는데, App에서는 ThemedButton이 아닌 Toolbar를 렌더링 하고 있기 때문에, 어쩔 수 없이 theme을 받아서 Toolbar에 전달해 주는 과정이 필요한 것
    → context로 이를 해결해 보자!!
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}
  • React.createContext()를 통해 'light'를 기본값으로 하는 context인 ThemeContext 생성
  • Provider를 통해 하위 트리에 값을 보내줄 수 있음
  • Provider로 값을 전달하면, 해당 컴포넌트 아래에 아무리 깊숙히 있어도, 모든 컴포넌트가 이 값을 읽을 수 있다!
  • 위 예시에서는, 'dark'를 현재 선택된 테마 값으로 보내고 있음
  • 이제 중간에 있는 Toolbar 컴포넌트에서 일일이 테마값을 넘겨줄 필요 X
  • ThemedButton에서는 현재 선택된 테마 값을 읽기 위해 contextType 지정
  • React가장 가까이 있는 테마 Provider를 찾아 그 값을 사용할 것
  • 이 말은 즉, 만약 Toolbar에서 또 Provider로 테마 값을 해당 값이 쓰였을 거라는 말인듯

✨ Context 를 사용하기 전에 고려할 것

  • context의 주된 용도는, 다양한 레벨에 네스팅된 많은 컴포넌트에게 데이터를 전달하는 것
  • 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>
  • 예를 들어, 이런 식으로 LinkAvatar 컴포넌트에, useravatarSizeprops로 전달해야 하는 Page 컴포넌트가 있다고 생각해 보자
  • 실제로 사용되는 곳은 맨 안쪽의 Avatar 컴포넌트 뿐인데, useravatarSize props를 여러 단계에 걸쳐 보내줘야 해서 번거로움
    Avatar 컴포넌트 자체를 넘겨주면 context를 사용하지 않고 이를 해결할 수 있다!!
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}
  • LinkAvatar 컴포넌트 자체를 userLink 변수에 담아 props로 전달해주는 방식
  • 이렇게 바꾸면 LinkAvataruseravatarSize props를 사용한다는 걸 알아야 하는 건, 가장 위에 있는 Page뿐!
  • 이런 제어의 역전(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}
    />
  );
}
  • 자식으로 둘 수 있는 컴포넌트 수에 제한은 X
  • 이 패턴을 사용하면, 자식 컴포넌트와 직속 부모를 분리(decouple)하는 문제는 대개 해결 가능
  • 더 나아가 render props를 이용하면, 렌더링 되기 전부터 자식 컴포넌트가 부모 컴포넌트의 소통하게 할 수도 있음
  • 하지만 같은 데이터를 트리 안의 여러 레벨에 있는 많은 컴포넌트에 주어야 할 때도 있음
    → 이처럼 "방송"이 필요할 때는 context 이용

✨ Context 관련 API

React.createContext

const MyContext = React.createContext(defaultValue);
  • Context 객체 생성
  • Context 객체를 구독하고 있는 컴포넌트를 렌더링할 때, React는 트리 상위에서 가장 가까이 있는 짝이 맞는 Provider로부터 현재값을 읽음
  • defaultValue 매개변수는 트리 안에서 적절한 Provider를 찾지 못했을 때만 쓰이는 값
  • Provider를 통해 undefined를 값으로 보낸다고 해도, 구독 컴포넌트들이 defaultValue를 읽지는 않고, 그대로 undefined를 읽지는 않는다는 점에 유의하자!
    → 예외 처리에 그대로 사용하기에는 까다로울듯

Context.Provider

<MyContext.Provider value={/* 어떤 값 */}>
  • Context 객체에 포함된 React 컴포넌트인 Provider는, context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할
  • value prop을 받아서 그 값을 하위 컴포넌트에 전달함
  • 값을 전달받을 수 있는 컴포넌트 수에 제한 없음!
  • Provider 하위에 또 다른 Provider 배치도 가능하며, 이 경우 하위 Provider의 값이 우선시됨 (가까이에 있으니까)
  • Provider 하위에서 context를 구독하는 모든 컴포넌트는, Providervalue prop이 바뀔 때마다 다시 렌더링됨!!!
  • Provider로부터 하위 consumer로의 전파는, shouldComponentUpdate 메서드가 적용되지 않으므로, 상위 컴포넌트가 업데이트를 건너 뛰더라도 consumer가 업데이트 됨
  • context 값이 바뀌었는지 여부는, Object.is와 동일한 매커니즘을 통해 이전 값와 새로운 값을 비교하여 측정됨
    → 그래서 "객체"value로 보내는 경우 문제가 생길 수 있음 (유의해서 사용하자)

Class.contextType

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* MyContext의 값을 이용한 코드 */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* ... */
  }
}
MyClass.contextType = MyContext;
  • React.createContext()로 생성한 Context 객체를, 원하는 클래스의 contextType 프로퍼티로 지정 가능
  • 이 클래스는 contextType으로 이 Context 객체를 가져요~ 라는 걸 알려주는 느낌인듯
  • 이 프로퍼티를 활용해서 클래스 안에 this.context를 이용해, 해당 context의 가장 가까운 Provider를 찾아 그 값을 읽을 수 있게 됨
  • 이 값은 render를 포함한 모든 컴포넌트 생명주기 메서드에서 사용 가능
  • 이 API를 사용하면 하나의 context만 구독 가능하다는 점에 유의하자
class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* context 값을 이용한 렌더링 */
  }
}
  • 이런 느낌으로 안에서 지정도 가능

Context.Consumer

<MyContext.Consumer>
  {value => /* context 값을 이용한 렌더링 */}
</MyContext.Consumer>
  • context 변화를 구독하는 React 컴포넌트
  • 이 컴포넌트를 사용하면, 함수 컴포넌트 안에서 context 구독 가능
  • Context.Consumer자식은 함수여야 함
  • 이 함수는 context의 현재값을 받고, React 노드 반환
  • 이 함수가 받는 value 매개변수 값은, 해당 context의 Provider 중 상위 트리에서 가장 가까운 Providervalue prop과 동일
  • 상위에 Provider가 없다면, value 매개변수 값은 createContext()에 보냈던 defaultValue

Context.displayName

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

<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools
  • Context 객체는 displayName 문자열 속성 설정 가능
  • React 개발자 도구는, 이 문자열을 이용해 context를 어떻게 보여줄지 결정함

✨ 예시

✔️ 값이 변하는 context

theme-context.js

export const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
};

export const ThemeContext = React.createContext(
  themes.dark // 기본값
);
  • 기본값으로 dark를 가짐

themed-button.js

import {ThemeContext} from './theme-context';

class ThemedButton extends React.Component {
  render() {
    let props = this.props;
    let theme = this.context;
    return (
      <button
        {...props}
        style={{backgroundColor: theme.background}}
      />
    );
  }
}
ThemedButton.contextType = ThemeContext;

export default ThemedButton;
  • ThemedButtoncontextType으로 ThemeContext를 가짐

app.js

import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';

function Toolbar(props) {
  return (
    <ThemedButton onClick={props.changeTheme}>
      Change Theme
    </ThemedButton>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: themes.light,
    };

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };
  }

  render() {
    return (
      <Page>
        <ThemeContext.Provider value={this.state.theme}>
          <Toolbar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
        <Section>
          <ThemedButton />
        </Section>
      </Page>
    );
  }
}

const root = ReactDOM.createRoot(
  document.getElementById('root')
);
root.render(<App />);
  • ThemeProvider 안에 있는 ThemedButton, 즉 Toolbar 안에 있는 ThemedButtonstate로부터 theme 값을 읽지만,
    Provider 밖에 있는 그냥 ThemedButton은 기본값인 dark 사용

✔️ 하위 컴포넌트에서 context 업데이트 하기

  • 컴포넌트 트리 하위 깊숙이 있는 컴포넌트에서 context를 업데이트 해야 할 때가 종종 있음
    context를 통해 메서드를 보내자!

theme-context.js

export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});
  • createContext에 보내는 기본값의 모양을, 하위 컴포넌트가 받고 있는 매개변수 모양과 동일하게 만들어야 하는 것 잊지 말자!!

theme-toggler-button.js

import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

export default ThemeTogglerButton;
  • ThemeTogglerButton은 context로부터 theme 값과 toggleTheme 메서드를 함께 받고 있음

app.js

import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };

    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}

function Content() {
  return (
    <div>
      <ThemeTogglerButton />
    </div>
  );
}

const root = ReactDOM.createRoot(
  document.getElementById('root')
);
root.render(<App />);
  • state업데이트 메서드도 포함되어 있으므로, 이것도 context Provider를 통해 전달됨
  • Providerstate 전체 넘겨줌

✔️ 여러 context 구독하기

  • 각 context마다 Consumer를 개별 노드로 만들도록 설계되어 있는데,
    이것은 context 변화를 위해 다시 렌더링하는 과정을 빠르게 유지하기 위함!
const ThemeContext = React.createContext('light');

const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}

function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}
  • 기본값이 'light'인 ThemeContext
  • 로그인한 유저 정보를 담고 있는 UserContext
  • context 초기값을 제공하는 App 컴포넌트
  • Content를 보면, ThemeContextUserContext 모두 구독하고 있는 것을 볼 수 있음!!
  • 둘 이상의 context 값이 함께 쓰이는 경우가 많으면, 그 값들을 한 번에 받는 render prop 컴포넌트를 만드는 것도 고려해 보자

✨ 주의사항

  • 다시 렌더링할지 여부를 정할 때, 참조(reference)를 확인하기 때문에,
    Provider의 부모가 렌더링 될 때마다 불필요하게 하위 컴포넌트가 다시 렌더링 되는 문제가 생길 수도 있음
class App extends React.Component {
  render() {
    return (
      <MyContext.Provider value={{something: 'something'}}>
        <Toolbar />
      </MyContext.Provider>
    );
  }
}
  • 예를 들어, 이런 App에서는 value가 바뀔 때마다 매번 새로운 객체가 생성되므로, Provider가 렌더링 될 때마다 그 하위에서 구독하고 있는 컴포넌트 모두가 다시 렌더링됨...
    → 이를 방지하기 위해 값을 부모의 state로 끌어올리자
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }

  render() {
    return (
      <MyContext.Provider value={this.state.value}>
        <Toolbar />
      </MyContext.Provider>
    );
  }
}

🙇 참고 자료

Context

profile
쉽게만살아가면재미없어빙고

0개의 댓글