[React] Context API 사용하기 (React.createContext)

권준혁·2020년 11월 1일
1

React

목록 보기
10/20
post-thumbnail

안녕하세요!
Context API에 대한 포스팅입니다.
보통 상위컴포넌트에서 하위컴포넌트로 데이터를 전달할 때 속성값이 사용됩니다. 가까운 거리의 컴포넌트끼리는 괜찮지만, 깊이기 깊어질 수록 코드를 반복적으로 작성해야 합니다.
기계적으로 컴포넌트마다 전달하고 전달하는 방식은 비효율적입니다.
이 때 Context API를 사용하면 중첩구조가 복잡한 상황이어도 비교적 쉽게 데이터를 전달할 수 있다.

createContext 함수 구조

React.createContext(defaultValue) => {Provider, Consumer}

createContext함수는 Provider과 Consumer를 반환합니다.

간단하게 사용해보기

import React from 'react'
import Message from './Message'
export const MessageContext = React.createContext('hello')
export default class MyComponent extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return(
            <div>
                <MessageContext.Provider value='안녕?'>
                    <Message />
                </MessageContext.Provider>
            </div>
        )
    }
}

MyComponent는 하위에 Message컴포넌트를 가지고 있는 상위컴포넌트입니다.
여기서 MessageContext를 생성합니다.
세번째 줄

export const MessageContext = React.createContext('hello')

MessageContext는 React.createContext가 반환하는 Provider와 Consumer를 가지고 있는 객체입니다.

그럼 하위에 있는 Message 컴포넌트를 작성해보겠습니다.

// Message.js
import React from 'react'
import {MessageContext} from "./MyComponent";
export default function Message () {
    return (
        <MessageContext.Consumer>
            {message=> <p>{`Message : ${message}`}</p>}
        </MessageContext.Consumer>
    )
}

아까 상위컴포넌트에서 생성했던 Context객체를 import해서 Consumer를 작성합니다.
Consumer에서 함수형태로 사용할 수 있습니다.
message는 아까 Provider에서 value로 전달한 '안녕?' 문자열이 됩니다!


shouldComponentUpdate가 무시된다.

shouldComponentUpdate는 렌더링 성능을 향상시킬 목적으로 불타입을 반환해 컴포넌트가 업데이트를 할지 하지않을지 결정할 수 있습니다.

앞서 예제에서
MyComponent라는 이름의 상위컴포넌트와 Message라는 하위컴포넌트가 있었습니다. 둘 사이에 Info라는 컴포넌트가 있다고 가정할 때, 이 컴포넌트가 shouldComponentUpdate생명주기에서 false를 반환해 업데이트를 않겠다고 선언할 때, 일반적인 경우라면 Message하위컴포넌트는 업데이트 되지 않아야 합니다.

하지만 이런 상황을 무시하고, Provider컴포넌트가 업데이트되면 하위의 Consumer를 작성한 Messsage같은 컴포넌트는 업데이트되게 됩니다.
중간컴포넌트 Info가 속성,상태값이 없고 PureComponent인 경우도 마찬가지입니다.

어찌보면 당연합니다. 속성값,상태값이 변경될경우 업데이트 하는 것은 UI데이터를 이용자에게 최신으로 유지하기 위한 목적인데, Context의 데이터가 변경됐음에도 중간컴포넌트에서 업데이트를 차단했다는 이유로 최신데이터로 업데이트 되지 않는다면 최신UI데이터를 사용자에게 보여준다는 목적을 잃게됩니다.


중첩해서 Context데이터 사용하기

이 번에는 여러개의 컨텍스트를 중첩해서 사용해보겠습니다.
상위컴포넌트 MyComponent입니다.

import React from 'react'
import Message from './Message'

export const LanguageContext = React.createContext('en')
export const ThemeContext = React.createContext('bright')

export default class MyComponent extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return(
            <div>
                <LanguageContext.Provider value='ko'>
                    <ThemeContext.Provider value='dark'>
                        <Message/>
                    </ThemeContext.Provider>
                </LanguageContext.Provider>
            </div>
        )
    }
}

두 종류의 컨텍스트를 만들었습니다. Language와 Theme에 대한 컨텍스트입니다. render메서드에서 Provider를 중첩으로 사용했습니다.

다음은 하위컴포넌트 Message입니다.

import React from 'react'
import {LanguageContext,ThemeContext} from "./MyComponent";

export default function Message () {
    return (
        <LanguageContext.Consumer>
            {lang => (
                <ThemeContext.Consumer>
                    {theme=>(
                       <div style={{width:'100%', height:'200px',backgroundColor:theme==='dark'? 'darkgray':'lightgray'}}>
                           {`LANGUAGE : ${lang==='en'?'ENGLISH':'한국어'}`}
                       </div> 
                    )}
                </ThemeContext.Consumer>
            )}
        </LanguageContext.Consumer>
    )
}

컨텍스트 객체 두개를 import했습니다. 사용할 때는 코드가 콜백처럼 지저분해지긴 하는 것 같습니다.

Provider에서 어떤 value를 제공했느냐에 따라 정상적으로 다른 화면이 보여집니다.

)

이렇게 데이터의 종류별로 컨텍스트를 만들어서 사용하면 중간의 컴포넌트가 업데이트를 하지 않도록 설정해 성능상 이점을 가질 수 있게됩니다.
컨텍스트 데이터가 변경되면 중간컴포넌트의 렌더링을 건너뛰고 Consumer컴포넌트만 렌더링 되기 때문입니다.


생명주기에서 컨텍스트데이터 사용하기

컨텍스트 데이터를 생명주기에서 사용하는 방법입니다.
클래스형컴포넌트의 contextType 정적 멤버변수에 컨텍스트 객체를 입력하면 클래스내에서 this.context로 접근할 수 있습니다.

const ThemeContext = React.createContext('dark');
class MyComponent extends React.Component{
    //...
    componentDidMount() {
        const theme = this.context;
    }
    //...
}
MyComponent.contextType=ThemeContext;

다만 이 방식은 하나의 컨텍스트만 연결할 수 있다는 단점이 있습니다. 여러개의 컨텍스트를 연결하는 방법은 고차 컴포넌트를 이용하는 것입니다.

고차컴포넌트로 여러개의 컨텍스트데이터 전달하기

고차함수가 함수를 입력받아 함수를 반환하는 것처럼, 고차 컴포넌트는 컴포넌트를 입력받아 어떤 기능을 수행하고 컴포넌트를 반환하는 것 입니다.

//...

export default props => (
    <LanguageContext.Consumer>
        {language=>(
            <ThemeContext.Consumer>
                {theme=>(
                    <MyComponent {...props} language={MyComponent} theme={theme}></MyComponent>
                )}
            </ThemeContext.Consumer>
        )}
    </LanguageContext.Consumer>
)

MyComponent자체를 export하는 코드에서
MyComponent를 반환하는 고차함수를 export하게 바꿨습니다.
Consumer컴포넌트를 이용해 컨텍스트데이터를 속성값으로 각각 넣습니다.
상위컴포넌트에서 다른 속성값도 넣을 수 있도록 감싸는 함수의 인자를 props로 설정했고 spreadSyntax로 속성값에 각각 들어가게 했습니다.

하위 컴포넌트에서 컨텍스트 데이터 수정하기

저번 포스팅에서 봤던 완전제어컴포넌트처럼 상위컴포넌트의 상태값을 수정하는 메서드를 하위컴포넌트로 전달합니다. 다른 점은 Context로 데이터를 전달 한다는 것, 따라서 중첩깊이가 깊어도 아래와 같은 방식으로 전달할 수 있습니다.

아래로 스크롤을 좀 내려서 실행화면을 먼저 보면 이해하기 쉽습니다.

상위컴포넌트

import React from 'react'
import Message from './Message'

export const LanguageContext = React.createContext({
    language : 'en',
    changeLanguage : ()=> {}
})
export const ThemeContext = React.createContext({
    theme : 'bright',
    changeTheme : () => {}
})
export default class MyComponent extends React.Component {
    constructor(props) {
        super(props)
        this.state={
            LanguageContext : {
                language : 'ko',
                changeLanguage : this.changeLanguage
            },
            ThemeContext : {
                theme : 'dark',
                changeTheme : this.changeTheme
            }
        }
    }
    changeLanguage = () => {
        const isEnglish = this.state.LanguageContext.language === 'en';
        const nextLanguage = isEnglish? 'ko' : 'en'
        this.setState({
            LanguageContext : {
                ...this.state.LanguageContext,
                language : nextLanguage,
            }
        })
    }
    changeTheme = () => {
        const isBright = this.state.ThemeContext.theme === 'bright';
        const nextTheme = isBright? 'dark' : 'bright'
        this.setState({
            ThemeContext : {
                ...this.state.ThemeContext,
                theme : nextTheme
            }
        })
    }
    render() {
        return(
            <div>
                <LanguageContext.Provider value={this.state.LanguageContext}>
                    <ThemeContext.Provider value={this.state.ThemeContext}>
                        <Message/>
                    </ThemeContext.Provider>
                </LanguageContext.Provider>
            </div>
        )
    }
}

길어서 복잡한 것 같지만, changeLanguage라는 함수와 changeTheme이라는 함수를 컨텍스트에 추가로 전달하는 것 말고는 바뀐건 없습니다.

하위 컴포넌트

import React from 'react'
import {LanguageContext,ThemeContext} from "./MyComponent";

export default function Message () {
    return (
        <LanguageContext.Consumer>
            {LanguageContext => (
                <ThemeContext.Consumer>
                    {ThemeContext=>{
                        const {language:lang , changeLanguage} = LanguageContext;
                        const {theme , changeTheme} = ThemeContext;
                        return(
                       <div style={{width:'100%', height:'200px',backgroundColor:theme!=='dark'? 'white':'darkgray'}}>
                           {`LANGUAGE : ${lang==='en'?'ENGLISH':'한국어'}`}
                           <br/>
                           <button onClick={changeLanguage}>change language</button>
                           <button onClick={changeTheme}>change theme</button>
                       </div> 
                    )}}
                </ThemeContext.Consumer>
            )}
        </LanguageContext.Consumer>
    )

상위컴포넌트에서 상태값으로 Context데이터들과 동일한 구조의 객체를 관리합니다. 그리고, 이벤트함수를 정의해서 하위 컴포넌트로 Context를 통해 전달합니다.
하위컴포넌트에서는 전달된 Context데이터 내의 함수를 통해 상위컴포넌트의 state를 조작할 수 있습니다.

실행화면

클릭! 클릭!

잘 실행됩니다!


주의할 점

setState 내부 중첩

중간에 이벤트함수에서 setState를 호출할 때,
명시적으로 내부에 중첩된 객체리터럴을 적지 않아서 한 번만 실행되고, 그 이후에 state안의 change이벤트함수가 없어지게 되는 실수가 있었습니다.
명시적으로 내부 객체안의 요소를 적어주지 않아서였습니다.

this.state = {
    LanguageContext : {
    //...
    },
    ThemeContext : {
    //...
    }
}

이런 구조일 때 LanguageContext,ThemeContext 둘 사이에는 병합이 되지만 각각의 내부의 중첩된 내용까지 병합시켜주진 않습니다.
setState({LanguageContext})를 하면 기존의 ThemeContext와 함께 병합됩니다만, LanguageContext는 객체 전체가 교체됩니다.
state가 중첩구조일 때는 명시적으로 적어줘야 한다는 것을 인지해야겠습니다.

변경 전

        this.setState({
            LanguageContext : {
                // 작성하지 않음
                language : nextLanguage,
            }
        })

변경 후

        this.setState({
            LanguageContext : {
            ...this.state.LanguageContext,
                language : nextLanguage,
            }
        })

불필요한 렌더링을 발생시키는 경우

상위 컴포넌트의 render함수입니다.

    render() {
        return(
            <div>
                <LanguageContext.Provider value={{this.state.LanguageContext}}>
                    <ThemeContext.Provider value={this.state.ThemeContext}>    // (★)
                        <Message/>
                    </ThemeContext.Provider>
                </LanguageContext.Provider>
            </div>
        )
    }

ThemeContext의 Provider와 LanguageContext의 Provider의 value를 봐주세요.

Provider의 value속성에 {{this.state.LanguageContext}} 처럼 객체를 생성해서 전달합니다. 이럴 경우 상위컴포넌트의 상태값이 변경되어 render가 호출될 때마다, 새로운 객체를 계속해서 생성합니다.
결과적으로 눈에 보이는 UI데이터가 변경되지 않았음에도 불구하고 계속해서 불필요한 렌더링을 하게됩니다.
(★) ThemeContext처럼 전달해야합니다.

읽어주셔서 감사합니다. 컨텍스트 데이터에 대해서 알아봤습니다!

profile
웹 프론트엔드, RN앱 개발자입니다.

0개의 댓글