리액트 기본 배우기 -컨텍스트에 대해-

1. 컨텍스트란 무엇인가

 

일반적인 리액트 앱에서 데이터가 컴포넌트의 props를 통해 부모에게 자식으로 단방향 전달되었다

 

하지만 여러 컴포넌트에 걸쳐 자주 사용되는 데이터의 경우, 기존 방식으로는 코드가 너무 복잡해지고 사용하기에 불편함이 많았다.

 

이런 과정에서 나온 것이 바로 컨텍스트(context)

 

컨텍스트는 리액트 컴포넌트들 사이에서 데이터를 기존의 props를 통해 전달하는 방식 대신에

 

컴포넌트 트리(component tree)를 통해 곧바로 컴포넌트에 전달하는 새로운 방식을 제공한다

 

이를 통해 어떤 컴포넌트든지 데이터에 쉽게 접근할 수 있다

 

 

 

위 그림은 props를 통해 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달하는 일반적인 방식

 

하지만 이 방식에서는 여러 컴포넌트에 걸쳐 자주 사용되는 데이터(로그인 여부, 프로필 정보 등)를 전달하려 하면 반복적인 코드가 많이 생기며 지저분해진다는 단점

 

예를 들어 위 그림에서 루트 노드에 있는 데이터를 C 컴포넌트로 전달하려면, 최소 2번 props로 전달해야한다.

 

만약 데이터를 전달하려는 컴포넌트가 10단계 밑에 있다면, 10번이나 props를 타고 하위 컴포넌트로 내려가야함

 

이런 불편한 점을 개선하기 위해 생겨난 것이 바로 컨텍스트

 

 

컨텍스트를 사용하면, 일일이 props로 전달할 필요 없이

 

위 그림처럼 데이터를 필요로 하는 컴포넌트에 곧바로 데이터를 전달할 수 있다.

 

그러면서 코드도 깔끔해지고 데이터를 한곳에서 관리해서 디버깅을 하기에도 유리하다

 

 

2. 컨텍스트를 언제 사용해야하는가

 

컨텍스트를 사용하면 어떤 컴포넌트든지 데이터에 쉽게 접근할 수 있다고는 했지만, 언제 사용해야 좋은가?

 

2-1) 여러개의 컴포넌트들이 접근해야 하는데이터?

 

여러 컴포넌트에서 자주 필요로 하는 데이터로는 사용자의 로그인 여부, 로그인 정보, UI 테마, 현재 선택된 언어 등이 있다.

 

예를 들어 웹사이트 상단에 위치한 내비게이션 바에 사용자의 로그인 여부에 따라

 

로그인 버튼과 로그아웃 버튼을 선택적으로 보여주고 싶다면, 현재 로그인 상태 데이터에 접근할 필요가 있다.

 

마찬가지로 UI 테마, 현재 선택된 언어 같은 데이터도 곳곳에 있는 컴포넌트에 접근이 자주 일어날 가능성이 높은 데이터이다.

 

이러한 데이터들을 기존 방식대로 컴포넌트의 props로 넘겨주면, 자식 컴포넌트의 자식 컴포넌트까지 계속해서 내려갈수밖에 없다.

 

 

function App(props) {
    return <Toolbar theme="dark"/>
}

function Toolbar(props) {
    // 이 컴포넌트는 ThemedButton에 theme를 넘겨주기 위해 'theme' prop을 가져야만 한다

    // 현재 테마를 알아야 하는 모든 버튼에 대해 props로 전달하는 것은 매우 비효율적

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

function ThemedButton(props) {
    return <Button theme={props.theme}/>
}

 

위 코드는 3개의 컴포넌트가 나온다.

 

가장 상위 컴포넌트인 App 컴포넌트에서는 Toolbar 컴포넌트를 사용

 

이때 theme라는 이름의 prop으로 현재 테마인 dark를 넘긴다.

 

Toolbar 컴포넌트에서는 ThemedButton 컴포넌트를 사용하고 있다.

 

근데 ThemedButton에서 현재 테마를 필요로 한다.

 

그래서 prop으로 받은 theme를 하위 컴포넌트인 ThemedButton 컴포넌트에 전달한다.

 

최종적으로 ThemedButton에서 props.theme로 데이터에 접근해서, 버튼에 어두운 테마를 입힌다.

 

이처럼 props로 데이터를 전달하는 것은 실제 데이터를 필요로 하는 컴포넌트까지의 깊이가 깊어질수록 복잡해진다.

 

반복적인 코드를 계속해서 작성해주면서 비효율적이고 직관적이지도 않다.

 

컨텍스트를 사용해서 위와 동일한 기능을 구현해보면...

 

//컨텍스트는 데이터를 매번 컴포넌트를 통해 전달할 필요 없이
//컴포넌트 트리로 곧바로 전달하게 해줌

//여기서 현재 테마를 위한 컨텍스트를 생성, 기본값은 '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 ThemedButton(props) {
    //리액트는 가장 가까운 상위 테마 Provider를 찾아서 해당되는 값을 사용
    //만약 해당되는 Provider가 없을 경우, 기본값(여기서는 'light') 사용
    //여기서는 상위 Provider가 있기 때문에 현재 테마의 값은 'dark'

    return (
        <ThemeContext.Consumer>
            {value => <Button theme={value}/>}
        </ThemeContext.Consumer>
    )
}

 

위 코드에서 먼저 React.createContext()로 ThemeContext라는 이름의 컨텍스트를 하나 생성함

 

컨텍스트를 사용할 컴포넌트의 상위 컴포넌트에서 Provider로 감싸주어야하는데, 

 

여기서는 최상위 컴포넌트인 App 컴포넌트를 ThemeContext.Provider로 감싸주었다.

 

이렇게하면, Provider의 모든 하위 컴포넌트가 얼마나 깊이 위치해 있는지 관계없이, 컨텍스트의 데이터를 읽을 수 있다.

 

컨텍스트를 사용한 코드를 보면 전체적으로 간결하고 깔끔하면서 직관적으로 바뀐 것을 확인할 수 있다.

 

이렇게 여러 컴포넌트에서 계속 접근이 일어날 수 있는 데이터들이 있는 경우에 컨텍스트를 사용하는 것이 좋다.

 

 

3. 컨텍스트를 사용할 때 고려할점?

 

컨텍스트는 다른 레벨의 많은 컴포넌트가 특정 데이터를 필요로 하는 경우에 주로 사용

 

그러나 무조건 컨텍스트를 사용하는 것이 좋은 것은 아니다

 

컴포넌트와 컨텍스트가 연동되면, 재사용성이 떨어지기 때문이다.

 

그래서 다른 레벨의 많은 컴포넌트가 데이터를 필요로 하는 경우가 아니면

 

기존에 사용하던 방식대로 props를 통해 데이터를 전달하는 컴포넌트 합성 방법이 더 적합하다.

 

// page 컴포넌트는 pagelayout 컴포넌트를 렌더링

<Page user={user} avatarSize={avatarSize}/>

//pagelayout 컴포넌트는 navigationbar 컴포넌트를 렌더링

<PageLayout user={user} avatarSize={avatarSize}/>

//navigationbar 컴포넌트는 link 컴포넌트를 렌더링

<NavigationBar user={user} avatarSize={avatarSize}/>

//link 컴포넌트는 avatar 컴포넌트를 렌더링

<Link href={user.permalink}>
    <Avatar user={user} size={avatarSize}/>
</Link>

 

위 코드에는 사용자 정보와 아바타 사이즈를 몇단계에 걸쳐서 하위 컴포넌트인 link와 avatar로 전달하는 page 컴포넌트가 있다.

 

여기에 가장 하위 레벨에 위치한 avatar 컴포넌트가 user와 avatarsize를 필요로 하므로,

 

이를 위해 여러 단계에 걸쳐 props를 통해 user와 avatarsize를 전달해 주고 있다.

 

하지만 이 과정은 굉장히 불필요하게 느껴진다.

 

또한 avatar 컴포넌트에 추가적인 데이터가 필요해지면, 해당 데이터도 추가로 여러 단계에 걸쳐서 넘겨주어야 하므로 굉장히 번거롭다.

 

여기서 컨텍스트를 사용하지 않고 이러한 문제를 해결하는 방법은, 

 

Avatar 컴포넌트를 변수에 저장해서 직접 넘겨주는 것이다.

 

그러면 중간 단계의 컴포넌트들은 user, avatarsize에 대해 전혀 몰라도 된다.

 

function Page(props) {
    const user = props.user;

    const userLink = (
        <Link href = {user.permalink}>
            <Avatar user = {user} size={props.avatarSize}/>
        </Link>
    )

    //page 컴포넌트는 pagelayout 컴포넌트를 렌더링
    //이때, props로 userlink를 함께 전달

    return <PageLayout userLink = {userLink}/>
}

//PageLayout 컴포넌트는 NavigationBar 컴포넌트를 렌더링
<PageLayout userLink={...}/>

//NavigationBar 컴포넌트는 props로 전달받은 userLink element를 리턴
<NavigationBar useLink={...}/>

 

 

위 코드에서는 user와 avatarSize가 props로 들어간 Avatar 컴포넌트를 userLink라는 변수에 저장한 뒤에 해당 변수를 하위 컴포넌트로 넘기고 있다.

 

이렇게 하면 가장 상위 레벨에 있는 page 컴포넌트만 avatar 컴포넌트에서 필요로 하는 user와 avatarSize에 대해 알고 있으면 된다.

 

이런 방식은 중간 레벨의 컴포넌트를 통해 전달해야하는 props를 없애고, 코드를 더욱 간결하게 만들어준다.

 

또한 최상위에 있는 컴포넌트에 좀 더 많은 권한을 부여해준다.

 

다만 모든 상황에 이 방식이 좋은 것은 아니다.

 

데이터가 많아질수록 상위 컴포넌트에 몰리기 때문에, 상위 컴포넌트는 점점 더 복잡해지며,

 

하위 컴포넌트는 너무 유연해진다.

 

앞에서 사용한 방법을 좀 더 응용해서 하위 컴포넌트를 여러 개의 변수로 나눠 전달할 수도 있다.

 

function Page(props) {
    const user = props.user;

    const topBar = (
        <NavigationBar>
            <Link href={user.permalink}>
                <Avatar user={user} size={props.avatarSize}/>
            </Link>
        </NavigationBar>
    )

    const content = <Feed user={user}/>

    return (
        <PageLayout
        topBar={topBar}
        content={content}
        />
    )
}

 

이 방식은 하위 컴포넌트의 의존성을 상위 컴포넌트와 분리할 필요가 있는 대부분의 경우에 적합하다

 

또한 렌더링 전에 하위 컴포넌트가 상위 컴포넌트와 통신해야 하는 경우, render props를 사용하여 처리할 수 있다.

 

하지만 어떤 경우에는 하나의 데이터에 다양한 레벨이 있는 중첩된 컴포넌트들의 접근이 필요할 수 있다.

 

이러한 경우 위 방식은 사용할 수 없고, 컨텍스트를 사용해야한다.

 

컨텍스트는 해당 데이터와 데이터의 변경사항을 모두 하위 컴포넌트들에게 broadcast(널리 알려주기)해주기 때문이다.

 

컨텍스트를 사용하기에 적합한 데이터의 대표적인 예는 현재 지역 정보, UI테마 그리고 캐싱된 데이터 등..

 

 

4. React.createContext

 

컨텍스트를 사용하기위해 가장 먼저 해야할 일은 컨텍스트를 생성하는 것

 

컨텍스트를 생성하기 위해 React.createContext() 함수를 사용

 

함수의 파라미터로 기본값을 넣어주면 컨텍스트 객체 생성

 

const MyContext = React.createContext(기본값);

 

리액트에서 렌더링이 일어날 때, 컨텍스트 객체를 구독하는 하위 컴포넌트가 나오면,

 

현재 컨텍스트의 값을 가장 가까이에 있는 상위 레벨의 Provider로부터 받아온다.

 

그런데 만약 상위 레벨에 매칭되는 Provider가 없으면, 이 경우에만 기본값이 사용

 

그러므로 기본값은 Provider없이 컴포넌트를 테스트할 때 유용하다.

 

기본값으로 undefined를 사용하면, 기본값이 사용되지 않는다.

 

 

5. Context.Provider

 

React.createContext()로 컨텍스트를 만들었다면, 이제 하위 컴포넌트들이 해당 컨텍스트의 데이터를 받을 수 있도록 설정해줘야한다.

 

이를 위해 사용하는 것이 Provider

 

데이터를 제공해주는 컴포넌트라고 이해하면 된다.

 

모든 컨텍스트 객체는 Provider라는 리액트 컴포넌트를 갖는다.

 

Context.Provider 컴포넌트로 하위 컴포넌트들을 감싸주면, 모든 하위 컴포넌트들이 해당 컨텍스트의 데이터에 접근할 수 있게 된다.

 

<Mycontext.Provider value={/* some value */}>

 

Provider 컴포넌트에는 value라는 prop이 있으며, 이것은 Provider 컴포넌트 하위에 있는 컴포넌트에 전달된다.

 

그리고 하위 컴포넌트들이 이 값을 사용하게 되는데,

 

하위 컴포넌트가 데이터를 소비한다는 의미를 가지고 있어서 이들은 consumer 컴포넌트라고 부른다.

 

consumer 컴포넌트는 컨텍스트 값의 변화를 지켜보다가, 만약 값이 변경되면 재렌더링 된다.

 

 

참고로 Provider 컴포넌트는 여러개의 consumer 컴포넌트와 연결될 수 있고,

 

여러개의 Provider 컴포넌트는 중첩되어 사용될 수 있다.

 

Provider 컴포넌트로 감싸진 모든 consumer 컴포넌트는 Provider의 value prop이 바뀔때마다, 재렌더링 된다.

 

값이 변경되었을때, 상위 컴포넌트가 업데이트 대상이 아니더라도

 

하위에 있는 컴포넌트가 컨텍스트를 사용한다면, 하위 컴포넌트에는 업데이트가 일어난다.

 

 

6. Provider.value에서 주의사항

 

컨텍스트는 재렌더링 여부를 결정할 때 레퍼런스 정보를 사용하므로, Provider의 부모 컴포넌트가 재렌더링 되었을 경우

 

의도치 않게 consumer 컴포넌트의 재렌더링이 일어날 수 있다.

 

function App(props) {
    return (
        <MyContext.Provider value={{ something: 'something' }}>
            <Toolbar />
        </MyContext.Provider>
    )
}

 

위 코드는 Provider 컴포넌트가 재렌더링될때마다, 모든 하위 consumer 컴포넌트의 재렌더링이 발생한다.

 

왜냐하면 value prop을 위한 새로운 객체가 매번 새롭게 생성되기 때문이다.

 

이를 방지하기 위해 value를 직접 넣는것보다는, 컴포넌트의 state로 옮기고 해당 state의 값을 넣어주는 것으로 바꿔준다.

 

function App(props) {
    const [value, setValue] = userState({something:'something'})

    retun (
        <MyContext.Provider value={value}>
            <Toolbar />
        </MyContext.Provider>
    )
}

 

 

7. Class.contextType

 

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;

 

클래스 컴포넌트에 있는 contextType 속성에는 React.createContext() 함수를 통해 생성된 컨텍스트 객체가 대입될 수 있다.

 

이 속성을 사용하게 되면, this.context를 통해 상위에 있는 Provider 중에서 가장 가까운 것의 값을 가져오게 된다.

 

위의 예제 코드에 나온것처럼 render() 함수를 포함한 모든 생명주기 함수 어디에서든지 this.context를 사용할 수 있다.

 

참고로 이 API를 통해서는 단 하나의 컨텍스트만을 구독할 수 있다.

 

 

8. Context.Consumer

 

consumer 컴포넌트는 앞에서 설명한 것처럼 컨텍스트의 데이터를 구독하는 컴포넌트이다.

 

클래스 컴포넌트에서는 위에 나온 Class.contextType을 사용하면 되고,

 

함수 컴포넌트에서는 Context.Consumer를 사용하여 컨텍스트를 구독할 수 있다.

 

<MyContext.Consumer>
    {value => /* 컨텍스트의 값에 따라 컴포넌트들을 렌더링 */}
</MyContext.Consumer>

 

컴포넌트의 자식으로 함수가 올 수 있는데, 이것을 function as a child라고 부른다.

 

Context.Consumer로 감싸주면 자식으로 들어간 함수가 현재 컨텍스트의 value를 받아서, 리액트 노드로 리턴한다.

 

이때 함수로 전달되는 value는 Provider의 value prop과 동일하다.

 

만약 상위 컴포넌트에 Provider가 없다면, 이 value 파라미터는 createContext()를 호출할 때 넣는 기본값과 동일한 역할을 한다.

 

 

9. function as a child

 

function as a child는 컴포넌트의 자식으로 함수를 사용하는 방법이다.

 

리액트에서는 기본적으로 하위 컴포넌트들을 children이라는 prop으로 전달해주는데, 

 

children으로 컴포넌트 대신 함수를 사용하여 아래와 같이 사용가능하다.

 

// children이라는 prop을 직접 선언하는 방식
<Profile children={name => <p>이름: {name}</p>} />

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

 

10. Context.displayName

 

컨텍스트 객체는 displayName이라는 문자열 속성을 가진다.

 

그리고 크롬의 리액트 개발자 도구에서는 컨텍스트의 Provider나 Consumer를 표시할 때, 이 displayName을 함께 표시해준다.

 

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

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

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

 

11. 여러개의 컨텍스트를 사용하는 방법

 

클래스 컴포넌트에서 Class.contextType을 사용하면 한번에 하나의 컨텍스트만 사용할 수 있다

 

여러개의 컨텍스트를 사용할려면, Context.Provider를 중첩해서 사용하는 방식으로 구현한다.

 

//테마를 위한 컨텍스트
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 />
            <Context />
        </div>
    )
}

//컨텍스트 컴포넌트는 두개의 컨텍스트로부터 값을 가져와서 렌더링

function Content() {
    return (
        <ThemeContext.Consumer>
            {theme => (
                <UserContext.Consumer>
                    {user => (
                        <ProfilePage user={user} theme={theme}/>
                    )}
                </UserContext.Consumer>
            )}
        </ThemeContext.Consumer>
    )
}

 

위 코드에서는 ThemeContext와 UserContext 2개의 컨텍스트가 나온다.

 

App 컴포넌트에서는 각 컨텍스트에 대해 2개의 Provider를 사용해서, 자식 컴포넌트인 Layout을 감싸주었다.

 

그리고 실제 컨텍스트의 데이터를 사용하는 Content 컴포넌트에서는 두개의 Consumer 컴포넌트를 사용하여 데이터를 전달하고 있다.

 

이렇게 하면 여러개의 컨텍스트를 동시에 사용한다.

 

하지만 2개 또는 그 이상의 컨텍스트의 값이 자주 함께 사용되면,

 

모든 값을 한번에 제공해주는 별도의 render prop 컴포넌트를 직접 만드는 것을 고려하는게 좋다.

 

 

12. useContext

 

함수 컴포넌트에서는 컨텍스트를 사용하기 위해 컴포넌트를 매번 Consumer 컴포넌트로 감싸주는 것보다 더 좋은 방법이 있다.

 

앞에서 배운 훅을 사용하는 것이다.

 

useContext() 혹은 함수 컴포넌트에서 컨텍스트를 쉽게 사용할 수 있게 해준다.

 

useContext() 훅은 React.createContext() 함수 호출로 생성된 컨텍스트 객체를 인자로 받아 현재 컨텍스트 값을 리턴한다.

 

function MyComponent(props) {
    const value = useContext(MyContext)

    return (
        ...
    )
}

 

useContext() 훅을 사용하면 다른 방식과 동일하게 컴포넌트 트리상에서 가장 가까운 상위 Provider로부터 컨텍스트의 값을 받아오게 된다.

 

만약 컨텍스트의 값이 변경되면, 변경된 값과 함께 useContext() 훅을 사용하는 컴포넌트가 재렌더링 된다.

 

그렇기 때문에 useContext() 훅을 사용하는 컴포넌트의 렌더링이 꽤 무거운 작업이면, 별도로 최적화 작업을 해줄 필요가 있다.

 

또한 useContext() 훅을 사용할때, 파라미터로 컨텍스트 객체를 넣어줘야한다.

 

//올바른 사용
useContext(MyContext)

//잘못된 사용
useContext(MyContext.Consumer)
useContext(MyContext.Provider)

 

13. 실습으로 따라하기

 

$npx create-react-app으로 프로젝트 생성

 

src 폴더 내에 chapter_14라는 이름의 폴더 하나 생성

 

그리고 ThemeContext.jsx 파일 생성하고 코드 작성

 

import React from 'react';

const ThemeContext = React.createContext()

ThemeContext.displayName = "ThemeContext"

export default ThemeContext;

 

여기서 컨텍스트의 초기값을 별도로 설정하지 않았고, 이후 Provider에서 값을 설정할 예정

 

개발자 도구를 통해 컨텍스트의 이름을 확인하기 위해 ThemeContext의 displayName값을 설정

 

동일 폴더에 MainContext.jsx 파일 만들고 코드 작성

 

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로부터 현재 설정된 테마 값을 받아와 실제 화면의 콘텐츠를 렌더링하는 역할을 한다.

 

또한 테마 변경 버튼을 누를 경우, ThemeContext로부터 받은 toggleTheme() 함수를 호출하여 ThemeContext의 값을 변경하는 역할도 한다.

 

여기서 ThemeContext의 값을 가져오기 위해 ThemeContext.Consumer 컴포넌트를 사용하는 방법 대신에 useContext() 훅을 사용했다.

 

마지막으로 동일 폴더에 DarkOrLight.jsx 파일 만들고 코드 작성

 

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;

 

DarkOrLight 컴포넌트는 방금 전에 만든 MainContent 컴포넌트를 자식으로 갖고 있는데,

 

이를 ThemeContext.Provider로 감싸서, ThemeContext의 값을 하위 컴포넌트들이 사용할 수 있도록 해준다.

 

만약 ThemeContext.Provider로 감싸주지 않으면, 하위 컴포넌트들이 ThemeContext 값을 가져올 수 없다.

 

그리고 ThemeContext의 값으로 들어가는 theme, toggleTheme() 함수는 자체적으로 관리하고 있다.

 

이제 렌더링하기 위해 index.js파일을 수정

 

import DarkOrLight from './chapter_14/DarkOrLight';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <DarkOrLight/>
  </React.StrictMode>
);

 

npm start로 실행해본다.

 

 

처음 화면은 흰색 배경에 검은 글씨인 light테마

 

여기서 버튼을 누르면...

 

 

검은색 배경에 흰색 글씨를 가진 dark테마가 된다..

 

크롬 개발자 도구를 열어서 컴포넌트 탭을 확인해보자.

 

 

 

한번 더 누르면 light로 바뀐다

 

TAGS.

Comments