[React] Context

문지은·2023년 7월 23일
0

React

목록 보기
13/24
post-thumbnail
post-custom-banner

Context

  • 컴포넌트들 사이에서 데이터를 props로 전달하는 것이 아닌 컴포넌트 트리를 통해 곧바로 데이터를 전달하는 방식
  • 어떤 컴포넌트든지 context에 있는 데이터에 쉽게 접근할 수 있음
  • props로 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달하면 여러 컴포넌트에 걸쳐서 자주 사용되는 데이터(로그인 여부, 프로필 정보 등)를 전달하려면 반복적인 코드가 많이 생기고 지저분해짐
  • 예를 들어 아래 그림에서 루트 노드에 있는 데이터를 C 컴포넌트로 전달하려면 최소 2번 props로 전달해야 함
  • 컨텍스트를 사용하면 일일이 props로 전달할 필요 없이 아래 그림처럼 데이터를 필요로 하는 컴포넌트에 곧바로 데이터를 전달할 수 있음

context를 사용하는 경우

  • 여러 컴포넌트에서 계속해서 접근이 일어날 수 있는 데이터들이 있는 경우
  • Provider의 모든 하위 컴포넌트가 얼마나 깊이 위치해 있는지 관계없이 컨텍스트의 데이터를 읽을 수 있음

예제 코드로 이해해보자

  • 현재 선택된 테마를 기존 방식대로 컴포넌트의 props로 전달하는 코드
    • 상위 컴포넌트인 App 컴포넌트에서 Toolbar 컴포넌트로 theme라는 이름의 prop으로 현재 테마인 dark를 넘김
    • Toolbar 컴포넌트에서는 ThemeButton 컴포넌트를 사용하는데 ThemeButton 컴포넌트에서 현재 테마를 필요로 함
    • 따라서 prop으로 전달받은 theme을 하위 컴포넌트인 ThemeButton 컴포넌트에 전달
function App(props) {
    return <Toolbar theme="dark" />
}

function Toolbar(props) {
    // 이 Toolbar 컴포넌트는 ThemeButton에 theme를 넘겨주기 위해서 'theme' prop을 가져야만 함
    // 현재 테마를 알아야하는 모든 버튼에 대해서 props로 전달하는 것은 굉장히 비효율적
    return (
        <div>
            <ThemeButton theme={props.theme} />
        </div>
    );
}

function ThemeButton(props) {
    return <Button theme={props.theme} />;
}
  • 위와 같은 방식은 실제 데이터를 필요로 하는 컴포넌트까지의 깊이가 깊어질수록 복잡해짐
  • context를 사용하면 이러한 방식을 개선할 수 있음
// context는 데이터를 매번 컴포넌트를 통해 전달할 필요 없이 컴포넌트 트리로 곧바로 전달
// 여기에서는 현재 테마를 위한 컨텍스트를 생성하며, 기본 값은 'light'
const ThemeContext = React.createContext('light');

// Provider를 사용하여 하위 컴포넌트들에게 현재 테마 데이터를 전달
// 모든 하위 컴포넌트들은 컴포넌트 트리 하단에 얼마나 깊이 있는지에 관계 없이 데이터를 읽을 수 있음
// 여기에서는 현재 테마 값으로 'dark' 전달
function App(props) {
    return (
        <ThemeContext.Provider value="dark">
            <Toolbar />
        </ThemeContext.Provider>
    );
}

// 이제 중간에 위치한 컴포넌트는 테마 데이터를 하위 컴포넌트로 전달할 필요가 없음
function Toolbar(props) {
    return (
        <div>
            <ThemeButton />
        </div>
    );
}

function ThemeButton(props) {
    // 리액트는 가장 가까운 상위 테마 Provider를 찾아 해당되는 값을 사용
    // 만약 해당되는 Provider가 없을 경우 기본값(여기에서는 'light') 사용
    // 여기에서는 상위 Provider가 있기 때문에 현재 테마의 값은 'dark'가 됨
    return (
        <ThemeContext.Consumer>
            {value => <Button theme={value}/>}
        </ThemeContext.Consumer>
    );
}
  • 위 코드에서는 먼저 React.createContext() 함수를 사용해서 ThemeContext라는 이름의 컨텍스트 생성
  • 컨텍스트를 사용할 컴포넌트의 상위 컴포넌트에서 Provider로 감싸주어야 하는데 여기에서는 최상위 컴포넌트인 App 컴포넌트를 ThemeContext.Provider로 감싸줌
  • 이렇게 하면 Provider의 모든 하위 컴포넌트가 얼마나 깊이 위치해 있는지 관계 없이 컨텍스트의 데이터를 읽을 수 있음

context 사용 전 고려해야할 점

  • 컴포넌트와 컨텍스트가 연동되면 재사용성이 떨어짐
  • 다른 레벨의 많은 컴포넌트가 데이터를 필요로 하는 경우가 아니라면, 기존 방식대로 props를 통해 데이터를 전달하는 것이 더 적합

Context API

React.createContext

  • 컨텍스트를 생성하기 위한 함수
  • 컨텍스트 객체를 리턴함
const MyContext = React.createContext(기본값);
  • 리액트에서 렌더링이 일어날 때 컨텍스트 객체를 구독하는 하위 컴포넌트가 나오면 현재 컨텍스트의 값을 가장 가까이에 있는 상위 레벨의 Provider로부터 받아움
    • 만약 상위 리벨에 매칭되는 Provider가 없다면 기본값 사용
    • 기본값으로 undefined를 넣으면 기본값이 사용되지 않음

Context.Provider

  • 모든 컨텍스트 객체는 Provider라는 컴포넌트를 갖고 있음
  • Provider 컴포넌트로 하위 컴포넌트들을 감싸주면 모든 하위 컴포넌트들이 해당 컨텍스트의 데이터에 접근할 수 있게 됨
<MyContext.Provider value={/* some value */}>
  • Provider에는 value라는 prop이 있으며, 이것이 데이터로써 하위에 있는 컴포넌트들에게 전달됨
    • 하위 컴포넌트는 데이터를 소비한다는 의미로 consumer 컴포넌트라고 부름
    • consumer 컴포넌트는 컨텍스트 값의 변화를 지켜보다가 값이 변경되면 재렌더링됨
    • 하나의 Provider 컴포넌트는 여러 개의 consumer 컴포넌트와 연결될 수 있으며 여러 개의 Provider 컴포넌트는 중첩되어 사용될 수 있음
  • 주의 사항
    • 컨텍스트는 재렌더링 여부를 결정할 때 레퍼런스 정보를 사용하기 때문에 Provider의 부모 컴포넌트가 재렌더링 되었을 경우 의도치 않게 consumer 컴포넌트의 재렌더링이 일어날 수 있음
    • 이를 방지하기 위해서는 value를 직접 넣어주는 것이 아니라 컴포넌트의 state로 옮기고 해당 state 값을 넣어주어야 함
// 수정 전
function App(props) {
    return (
        <MyContext.Provider value={{something: 'something'}}>
            <Toolbar />
        </MyContext.Provider>
    )
}

// 수정 후
function App(props) {
    const [value, setValue] = useState({ something: 'something' });
    return (
        <MyContext.Provider value={value}>
            <Toolbar />
        </MyContext.Provider>
    )
}

Class.contextType

  • Provider 하위에 있는 클래스 컴포넌트에서 컨텍스트의 데이터에 접근하기 위해 사용
  • 단 하나의 컨텍스트만을 구독할 수 있음
  • 아래 코드 처럼 MyClass.contextType = MyContext;라고 해주면 MyClass 컴포넌트는 MyContext 데이터에 접근 가능
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;
        // MyContext 값에 따라 컴포넌트들을 렌더링
    }
}
MyClass.contextType = MyContext;

Context.Consumer

  • 컨텍스트의 데이터를 구독하는 컴포넌트
  • 데이터를 소비한다는 뜻에서 consumer 컴포넌트라고도 부름
<ThemeContext.Consumer>
    {value => /* 컨텍스트의 값에 따라서 컴포넌트들을 렌더링 */>}
</ThemeContext.Consumer>
  • consumer 컴포넌트는 컨텍스트 값의 변화를 지켜보다가 값이 변경되면 재렌더링됨
  • 하나의 Provider 컴포넌트는 여러 개의 consumer 컴포넌트와 연결될 수 있음
  • 상위 레벨에 매칭되는 Provider가 없을 경우 기본값이 사용

function as a child

  • 컴포넌트의 자식으로 함수를 사용하는 방법
  • 리액트에서는 기본적으로 하위 컴포넌트들을 children이라는 prop으로 전달해주는데 children으로 컴포넌트 대신 함수를 사용하여 아래와 같이 사용
// children이라는 prop을 직접 선언하는 방식
<Profile children={name => <p>이름 : {name}</p>} />

// Profile 컴포넌트로 감싸서 children으로 만드는 방식
<Profile>{name => <p>이름: {name}</p>}</Profile>

Context.displayName

  • 컨텍스트 객체는 displayName이라는 문자열 속성을 가짐
  • 크롬의 리액트 개발자 도구에서는 컨텍스트의 Provider나 Consumer를 표시할 때 이 displayName을 함께 표시해줌
  • 예를 들어 아래와 같이 코드를 작성하면 MyDisplayName이 리액트 개발자 도구에 표시됨
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

// 개발자 도구에 "MyDisplayName.Provider"로 표시됨
<MyContext.Provider>
  
// 개발자 도구에 "MyDisplayName.Consumer"로 표시됨
<MyContext.Consumer>

여러 개의 context 사용하기

  • Provider 컴포넌트와 Consumer 컴포넌트를 여러 개 중첩해서 사용하면 됨

예제 코드

// 테마를 위한 컨텍스트
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>
    );
}
  • 위 코드에서는 App 컴포넌트에서는 ThemeContext와 UserContext 두 개의 컨텍스트에 대해 두 개의 Provider를 사용하여 자식 컴포넌트인 Layout을 감싸줌
  • 실제 사용하는 Content 컴포넌트에서 두 개의 Consumer 컴포넌트를 사용하여 데이터 전달
  • 이렇게 하면 여러 개의 컨텍스트를 동시에 사용 가능
    • 하지만 두 개 또는 그 이상의 컨텍스트의 값이 자주 함께 사용될 경우 모든 값을 한 번에 제공해주는 별도의 render prop 컴포넌트를 직접 만드는 것이 더 좋음

useContext()

  • 함수 컴포넌트에서 컨텍스트를 쉽게 사용할 수 있게 해주는 훅
  • React.createContext() 함수 호출로 생성된 컨텍스트 객체를 인자로 받아서 현재 컨텍스트의 값을 리턴
  • 컨텍스트의 값이 변경되면 변경된 값과 함께 useContext() 훅을 사용하는 컴포넌트가 재렌더링됨
function MyComponent(props){
	const value = useContext(MyContext);
  
  	return (
    	...
    )
}
  • useContext() 훅을 사용할 때에는 파라미터로 컨텍스트 객체를 넣어줘야 함
    • Consumer나 Provider를 넣으면 안됨!
useContext(MyContext);

실습 - 컨텍스트를 사용하여 테마 변경 기능 만들기

  • 컨텍스트를 사용하여 테마를 변경할 수 있는 기능을 만들어보자!

컨텍스트 만들기

  • 초기값을 별도로 설정하지 않고, 개발자 도구를 통해 컨텍스트 이름을 확인하기 위해 ThemeContextdisplayName 값을 설정해준다.
import React from "react";

const ThemeContext = React.createContext();
ThemeContext.displayName = "ThemeContext";

export default ThemeContext;

자식 컴포넌트 만들기

  • ThemeContext로부터 현재 설정된 테마 값을 받아와 실제 화면의 콘텐츠를 렌더링하는 컴포넌트
  • 테마 변경 버튼을 누를 경우 ThemeContext로부터 받은 toggleTheme() 함수를 호출하여 ThemeContext의 값을 변경하는 역할도 함
  • 여기서는 ThemeContext.Consumer이 아닌 useContext() 훅을 사용하여 ThemeContext 를 받아옴
import { useContext } from "react";
import ThemeContext from "./ThemeContext";

function MainContent(props) {
    const { theme, toggleTheme } = useContext(ThemeContext);

    return (
        <div
            style={{
                width: "100vw",
                height: "100vh",
                padding: "1.5rem",
                backgroundColor: theme == "light" ? "white" : "black",
                color: theme == "light" ? "black" : "white",
            }}
        >
            <p>안녕하세요, 테마 변경이 가능한 웹사이트 입니다.</p>
            <button onClick={toggleTheme}>테마 변경</button>
        </div>
    );
}

export default MainContent;

부모 컴포넌트 만들기

  • 자식 컴포넌트 MainContent 컴포넌트를 ThemeContext.Provider로 감싸 ThemeContext 값을 하위 컴포넌트들이 사용할 수 있도록 함
import { useState, useCallback } from "react";
import ThemeContext from "./ThemeContext";
import MainContent from "./MainContent";

function DarkOrLight(props) {
    const [theme, setTheme] = useState("light");

    const toggleTheme = useCallback(() => {
        if (theme == "light") {
            setTheme("dark");
        } else if (theme == "dark") {
            setTheme("light");
        }
    }, [theme]);

    return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
            <MainContent />
        </ThemeContext.Provider>
    );
}

export default DarkOrLight;

실행 결과

  • 처음화면은 흰색 배경에 검은 글씨인 light 테마로 나타나다가 테마변경 버튼을 누르면 아래 그림과 같이 검은색 배경에 흰색 글씨인 dark 테마로 바뀌는 것을 볼 수 있음

  • 개발자 도구를 확인해보면 위에서 설정한 ThemeContext.displayName이 표시되는 것을 볼 수 있음

실습 전체 코드

References

profile
코드로 꿈을 펼치는 개발자의 이야기, 노력과 열정이 가득한 곳 🌈
post-custom-banner

0개의 댓글