본문 바로가기

react

[소플] ch 14. 컨텍스트

1. 컨텍스트란 무엇인가?

 

props를 이용한 방식 

 

- 데이터가 컴포넌트의 props를 통해 부모에서 자식으로 단방향 전달

- 여러 컴포넌트에 걸쳐서 자주 사용되는 데이터를 전달하려 하면 반복적인 코드가 많이 생기고 지저분해짐

 

컨텍스트를 이용한 방법 

 

- 컴포넌트 트리 (component tree)를 통해 곧바로 컴포넌트에 전달 

- 컨텍스트를 사용하면 일일이 props로 전달할 필요 없이 데이터를 필요로 하는 컴포넌트에 곧바로 데이터를 전달할 수 있음

- 해당 데이터와 데이터의 변경사항을 모두 하위 컴포넌트들에게 broadcast 해줌

- 컨텍스트를 사용하기에 적합한 데이터의 대표적인 예로 현재 지역정보, UI 테마, 캐싱된 데이터 등이 있음

 

 

 

 

2. 언제 컨텍스트를 사용해야 할까?

 

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

 

ex)

- 현재 선택된 테마를 전달

 

1) 기존의 props을 이용 

 

- 실제 데이터를 필요로 하는 컴포넌트까지의 깊이가 길어질수록 복잡 

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} />;
}

 

2) 컨텍스트 사용

 

- React.createContext() 함수를 사용해서 ThemeContext라는 이름의 컨텍스트를 생성

 

- 컨텍승트를 사용할 컴포넌트의 상위 컴포넌트에서 Provider로 감싸주어야 하므로 APP 컴포넌트를 ThemeContext.Provider로 감싸줌 

 -> Provider의 모든 하위 컴포넌트가 얼마나 갚이 위치해 있는지 관계없이 컨텍스트의 데이터를 읽을 수 있음 

 

//테마를 위한 컨텍스트를 생성하며, 기본값은 '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가 없을 경우 기본값을 사용 
    //여기에서는 상위 Provider가 있기 때문에 현재 테마의 값은 "dark"가 됨
    return (
        <ThemeContext.Consumer>
            {value=><Button theme={value} />}
        </ThemeContext.Consumer>
    )
}

 

 

3. 컨텍스트를 사용하기 전에 고려할 점 

 

- 무조건 컨텍스트를 사용하는 것이 좋은 것은 아님

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

 

- 다른 레벨의 많은 컴포넌트가 데이터를 필요로 하는 경우가 아니라면 props를 통해 데이터를 전달하는 컴포넌트 합성 방법이 더 적합 

 

ex)

 

- 가장 하위 레벨에 위치한 Avatar 컴포넌트가 user와 avatarSize를 필요로 하기 때문에, 이를 위해 여러 단계에 걸쳐서 props를 통해 user와 avatarSize를 전달해 주고 있는 상황

//page 컴포넌트는 PageLayout 컴포넌트를 렌더링
<Page user={user} avatarSize={avartarSize} />

//PageLayout 컴포넌트는 NavigationBar컴포넌트를 렌더링 
<PageLayout user={user} avatarSize={avartarSize} />

//NavigationBar 컴포넌트는 Link 컴포넌트를 렌더링
<NavigationBar user={user} avatarSize={avartarSize} />

//Link 컴포넌트는 Avatar 컴포넌트를 렌더링 
<Link href={user.permalink}>
    <Avatar user={user} size={avatarSizes} />
</Link>

 

1) 엘리먼트 변수 사용 

 

- user와 avartarSize가 props로 들어간 Avatar 컴포넌트를 userLink라는 변수에 저장한 뒤에 해당 변수를 하위 컴포넌트로 넘김 

- 가장 상위 레벨에 있는 Page 컴포넌트만 Avatar 컴포넌트에서 필요로 하는 user와 avartarSize에 대해 알고 있으면 됨 

 

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 userLink={...} />


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

 

2) 하위 컴포넌트를 여러 개의 변수로 나눠서 전달

 

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

- 렌더링 전에 하위 컴포넌트가 상위 컴포넌트와 통신해야 하는 경우 render 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}
        />
    );

}

 

 

4. 컨텍스트 API

 

1) React.createContext

 

- 컨텍스트 생성

- 함수의 파라미터로 기본값을 넣어줌 

- 컨텍스트 객체 리턴 

 

- 리액트에서 렌더링이 일어날 떄, 컨텍스트 객체를 구독하는 하위 컴포넌트가 나오면 현재 컨텍스트의 값을 가장 가까이에 있는 상위 레벨의 Provider로부터 받아오게 됨 

- 만약 상위 레벨에 매칭되는 Provider가 없다면, 기본값이 사용 

- 기본값으로 Undefined를 넣으면 기본값이 사용되지 않음 

 

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

 

2) Context.Provider

 

- 모든 컨텍스트 객체는 Provider라는 리액트 컴포넌트를 갖고 있음 

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

 

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

- Provider 컴포넌트에는 value라는 prop이 있으며, 이것은 Provider 컴포넌트 하위에 있는 컴포넌트들에게 전달 

 

- 하위 컴포넌트들을 consumer 컴포넌트라고 부름

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

 

- 하나의 Provider 컴포넌트는 여러 개의 consumer 컴포넌트와 연결될 수 있으며 여러 개의 Provider 컴포넌트는 중첩되어 사용될 수 있음 

 

 

3) Context.Consumer

 

- 컨텍스트의 데이터를 구독하는 컴포넌트 

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

 

function as a child

 

- 컴포넌트의 자식(child)으로 함수를 사용하는 방법 

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

- 함수로 전달되는 value는 Provider의 value prop과 동일 

 

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

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

 

 

4) Context.displayName

 

- 컨텍스트 객체는 displayName이라는 문자열 속성을 가짐 

- 크롬의 리액트 개발자 도구에서는 컨텍스트의 Provider나 Consumer을 표시할 때 displayName을 함께 표시 

 

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

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

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

 

 

5. 여러 개의 컨텍스트 사용하기 

 

- Context.Provider을 중첩해서 사용하는 방식으로 구현 

 

ex)

 

- App 컴포넌트에서는 각 컨텍스트에 대해 두 개의 Provider를 사용하여 자식 컴포넌트인 Layout을 감싸줌 

- 실제 컨텍스트의 데이터를 사용하는 Content 컴포넌트에서는 두 개의 Consumer 컴포넌트를 사용하여 데이터를 전달 

 

//테마를 위한 컨텍스트 
const ThemeContext = React.createContext('light');

//로그인을 한 사용자를 위한 컨텍스트 
const UserContext = React.createContext( {
    name: 'Guest',
});

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

        return (
            <ThemeContext.Provider value={theme} >
                <UserContext.Provider value={singedInUser}>
                    <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>
    );
}

 

 

6. useContext

 

- useContext() 훅은 함수 컴포넌트에서 컨텍스트를 쉽게 사용할 수 있게 해줌

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

 

function MyComponent(props) {
    const value = useContext(MyContent);
    
    return (
    	...
     )
}

 

 

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

 

 

ThemeContext 컴포넌트 

 

- ThemeContext 생성 

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

import React from "react";

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

export default ThemeContext;

 

MainContent 컴포넌트 

 

- ThemeContext로부터 현재 설정된 테마 값을 받아와 실제 화면의 콘텐츠를 렌더링

- 테마 변경 버튼을 누를 경우 ThemeContext로 부터 받은 toggleTheme() 함수를 호출하여 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;

 

 

DarkOrLight 컴포넌트 

 

- 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;

'react' 카테고리의 다른 글

[리액트를 다루는 기술] 3장 컴포넌트  (1) 2023.11.01
[소플] ch 15. 스타일링  (0) 2023.11.01
[소플] ch 13. 합성 vs 상속  (0) 2023.10.30
[소플] ch 12. State 끌어올리기  (0) 2023.10.30
[소플] ch 11. 폼  (0) 2023.10.29