Dog foot print

useContext 조금 더 자세히 사용해보기 본문

REACT

useContext 조금 더 자세히 사용해보기

개 발자국 2023. 3. 30. 09:38

useContext란 ?

useContext는 react에서 제공하는 훅으로, 하위 컴포넌트에게 context를 제공하는 훅입니다. “context”란 직역하면 환경이라는 의미이지만, 여기서는 리액트에서는 각 컴포넌트가 참조할 수 있는 데이터 모음이라고 생각하시면 될 것 같습니다.

아래의 코드는 프롭스를 하위 컴포넌트로 전달하는 예시입니다.

 

import React from 'react'

function End({text} : {text : string}){
    return (
        <div>
            {text}
        </div>
    )
}

function Middle({text} : {text : string}){
    return (
        <div>
            <End text={text}/>
        </div>
    )
}

export default function App() {

    const text = "Hello world";

    return (
        <div>
            <Middle text={text}/>
        </div>
    )
}

위 코드를 보면 End 컴포넌트에서 text를 사용하기 위해서 App -> Middle -> End 순으로 props를 계속 전달해주어야 합니다. 이를 이미지로 표현 하면 다음과 같습니다.

 

 

위의 코드에서는 한개의 노드를 거쳐 text 프롭스가 전달되지만, 실제로 앱을 구성하게 되면저렇게 단순한 구조를 가지는 것이 아니라, 굉장히 깊고 브런치들이 많습니다. 그렇기에 값을 사용하는 컴포넌트까지 값을 운반하는 것은 매우 불편하고 어렵습니다. 다음은 위 처럼 props를 계속해서 전달해주었을 때, 나타나는 단점 목록입니다.

  1. 중간 컴포넌트에서 데이터를 변환하여 전달할 가능성이 있다.
  2. props를 계속 전달해주어야 하기 때문에 컴포넌트 가독성이 떨어진다.
  3. 컴포넌트가 다른 컴포넌트와 강한 의존성을 가질 수 있다.

위와 같은 문제점으로 인하여, props를 계속 전달하는 행위에서 벗어나, 상위 컴포넌트에서 하위 컴포넌트들을 상태와 함께 감싸 상태를 공유 할 수 있는 context 라는 개념이 발생되었다.

 

아래는 context를 설명한 이미지이다.

 

useContext 사용해보기

리액트에서는 “context”를 생성하기 위한 createContext와 하위 컴포넌트에서 “context”에서 값을 읽고 변화를 감지하기 위한 useContext 를 제공해주고 있다.

먼저 “createContext”의 타입은 다음과 같다.

 

  function createContext<T>(defaultValue: T): Context<T>;
    type Provider<T> = ProviderExoticComponent<ProviderProps<T>>;
    type Consumer<T> = ExoticComponent<ConsumerProps<T>>;
    interface Context<T> {
        Provider: Provider<T>;
        Consumer: Consumer<T>;
        displayName?: string | undefined;
    }

createContext를 호출 할 때 인자로 “context”의 초기값을 전달 한 다음 리턴된 Context를 이용하여, 하위 컴포넌트를 감싸게 됩니다.

아래는 “context”를 생성한 뒤 하위 컴포넌트를 “context”로 감싸는 예시 코드입니다. (설명을 위해서, useContext는 사용하지 않았습니다.)

 

import React,{createContext} from 'react'

const TextContext = createContext("Hello world")

function End(){
    return (
        <div>
            {''}
        </div>
    )
}

function Middle(){
    return (
        <div>
            <End/>
        </div>
    )
}

export default function App() {

    return (
        <div>
            <TextContext.Provider value={"Hello world"}>
                <Middle/>
            </TextContext.Provider>
        </div>
    )
}

아직까지는 큰 변화는 없습니다. 단순히, “App”에서 “context”로 하위 컴포넌트를 감싼 형태입니다. 하위 컴포넌트에서 context에 접근하여 값을 사용하려면 context를 사용하는 component에서 useContext를 사용해야 합니다.

 

다음은 “end”컴포넌트에서 useContext로 TextContext를 사용한 코드와 결과물 이미지 입니다.

import React,{createContext, useContext} from 'react'

const TextContext = createContext("Hello world")

function End(){
    const text = useContext(TextContext);
    return (
        <div>
            {text}
        </div>
    )
}

function Middle(){
    return (
        <div>
            <End/>
        </div>
    )
}

export default function App() {
    return (
        <div>
            <TextContext.Provider value={"Hello world"}>
                <Middle/>
            </TextContext.Provider>
        </div>
    )
}

 

 

 

context 값 변경하기

“Context” 의 값을 변경하는 것은 매우 간단합니다. 바로 “provider”의 value를 업데이트를 하면 이를 구독하는 컴포넌트가 다시 렌더링합니다.

아래의 코드는 “input”으로 변경된 상태 값을 provider의 value로 전달하는 예시와 결과입니다.

import React,{createContext, useContext, useState} from 'react'

const TextContext = createContext("Hello world")

function End(){
    const text = useContext(TextContext);
    return (
        <div>
            {text}
        </div>
    )
}

function Middle(){

    return (
        <div>
            <End/>
        </div>
    )
}

export default function App() {

    const [text, setText] = useState("Hello world")

    return (
        <div>
            <TextContext.Provider value={text}>
                <Middle/>
            </TextContext.Provider>
            <input type="text" onChange={(e) => setText(e.target.value)} />
        </div>
    )
}

 

 

 

이런 걱정이 들 수 있습니다. Provider로 감싸진 하위 컴포넌트들이 상위 컴포넌트가 변경된 경우 “useContext를 사용하지 않는 컴포넌트들도 렌더링이 일어나는 것이 아닌가” ? 라는 의문이 발생 할 수 있습니다. 결과부터 말씀드리면 Provider상태가 변경되면 하위 컴포넌트는 모두 렌더링이 발생됩니다.

 

미들 컴포넌트에 렌더링 될 때 마다 console.log를 사용할 수 있도록 코드를 입력 후 결과를 확인해보겠습니다.

 

 

provider가 변화하면 하위 컴포넌트가 모두 재 렌더링 되는 이유는 contextAPI는 하위 컴포넌트에게 props를 계속 전달해줘야하는 props drilling을 해결하기 위한 방법이기 때문입니다.

context를 사용하는 컴포넌트만 업데이트 하기

만약 자주 변경될 여지가 있는 “context”가 많은 컴포넌트를 감싸고 있고, 하위 컴포넌트도 많은 컴포넌트를 하위로 두고 있다면, 다시 렌더링되는 과정에서 앱의 성능하락으로 이어질 것이다. 그렇기에 “context”를 직접 사용하지 않는 컴포넌트가 “context”에 의해서 다시 렌더링 되는 것은 지양해야합니다.

 

위의 예제에서 “Midldle” 컴포넌트가 ‘TextContext”가 변경될 때 마다 렌더링 되는 이유는 상위 “App” 컴포넌트에서 context가 변화하여, 다시 “Middle” 컴포넌트를 호출하기 때문입니다.

 

“Middle” 컴포넌트가 사용하지 않는 context 때문에 다시 호출 되지 않기 위해서는 리액트에서 제공하는 Memo 함수를 사용하면 됩니다. “Memo”함수의 타입은 다음과 같습니다.

 

  function memo<P extends object>(
        Component: FunctionComponent<P>,
        propsAreEqual?: (prevProps: Readonly<P>, nextProps: Readonly<P>) => boolean
    ): NamedExoticComponent<P>;

이 함수는 첫번째 인자로 함수형 컴포넌트 를 제공 받고, 두번째 인자로는 이전 props와 새로 받은 props를 인자로 두고 호출 여부를 반환하는 콜백을 받습니다. 이 “memo” 함수를 요약하면 다음과 같습니다.

 

렌더링할 컴포넌트에 해당하는 props와 다음 props를 비교하여, 재 호출 여부를 결정하는 함수

 

아래는 memo함수를 이용해서, “Middle” 컴포넌트를 감싼 코드 입니다.

import React,{createContext, useContext, useState,memo} from 'react'

const TextContext = createContext("Hello world")

function End(){
    const text = useContext(TextContext);
    return (
        <div>
            {text}
        </div>
    )
}

const MemoizedMiddle = memo(Middle);

function Middle(){
    console.log("Middle Render")
    return (
        <div>
            <End/>
        </div>
    )
}

export default function App() {

    const [text, setText] = useState("Hello world")

    return (
        <div>
            <TextContext.Provider value={text}>
                <MemoizedMiddle></MemoizedMiddle>
            </TextContext.Provider>
            <input type="text" onChange={(e) => setText(e.target.value)} />
        </div>
    )
}

 

memo 함수 두번째 인자로 콜백을 전달하지 않았습니다. 이로 인하여, “memo”로 감싸진 “Middle” 컴포넌트는 자신의 상태를 변경하지 않는 이상 다시 렌더링 되거나 상위에서 다시 호출 되지 않습니다. 아래는 위 코드의 결과 입니다.

 

 

결론

컨텍스트 api가 변경되면 하위 컴포넌트들이 모두 재 호출 되기 때문에, 쓸대없는 렌더링을 막기 위해서는 memo와 같은 방어수단이 필요하다. 이런 이유로 contextAPI를 사용하면 하위 컴포넌트의 재 호출을 막기위해서 컴포넌트를 감싸는 코드를 계속 생산해야 한다. 이는 코드의 가독성을 해치고 생산성이 낮아지는 결과를 불러옵니다.

 

위의 이유말고도 context를 논리적으로 분리하였다면 context의 중첩은 필연적이게 되어 코드의 가독성을 해치게 됩니다. 아래는 중첩된 “context”의 예시 코드입니다. 

 


const TextContext = createContext("Hello world")
const NumbContext = createContext(1);
const BoolContext = createContext(true)

export default function App() {

    const [text, setText] = useState("Hello world")

    return (
        <div>
            <TextContext.Provider value={text}>
                <NumbContext.Provider value={1}>
                    <BoolContext.Provider value={true}>
                        <MemoizedMiddle></MemoizedMiddle>
                    </BoolContext.Provider >
                </NumbContext.Provider>
            </TextContext.Provider>
            <input type="text" onChange={(e) => setText(e.target.value)} />
        </div>
    )
}

 

위와 같은 이유들 때문에 context는 단점들이 많은 것 같지만 여전히 props drilling은 매력적이다. 그렇기에, context api는 상태변화가 거의 일어나지 않을 것으로 예상되는 테마와 같은 값을 전달하기에 적합합니다.

반응형
Comments