본문 바로가기
나도 공부한다/React

React의 Context API 알아보기

by 꾸빵이 2024. 1. 12.

 

목차

    리액트에서의 데이터 흐름과 발생하는 문제

    리액트는 부모에서 자식으로 데이터를 전달하는 단방향 데이터 흐름을 갖고 있다.

     

    따라서 A - B - C 컴포넌트가 있다고 가정했을 때(A의 하위 컴포넌트는 B, B의 하위 컴포넌트는 C) A에서 C에게 데이터를 전달하려면 B를 반드시 거쳐야한다. B에서는 A의 데이터를 쓰지 않는데 오직 전달만을 위해 데이터를 받아야하는 비효율적인 동작을 한다.

     

    이렇게 여러번 props를 전달해야하는 문제를 props drilling이라고 한다.

     

    이는 리액트의 context API를 쓰면 해결할 수 있다. 

     


    Context가 뭐지?

    우선 동작 방식은 다음과 같다.

    1. 모든 데이터를 갖고 있는 컴포넌트가 provider라는 자식 컴포넌트에게 자신이 갖고 있는 모든 데이터를 준다.

    2. provider은 자신의 자식에 해당하는 모든 컴포넌트에게 직통으로 데이터를 줄 수 있다. 즉, 데이터 공급자 역할을 한다.

    3. 자식 컴포넌트들은 provider에게 직통으로 데이터를 공급 받는다.

     

    이해를 위해 간단한 구조를 그려봤다.

     

    provider 컴포넌트 아래에 있는 모든 컴포넌트들은 데이터를 관리하기 위한 문맥 속에서 살아간다. 따라서 context라고 불린다. 반대로 provider의 자식으로 배치되지 않은 컴포넌트들은 당연히 provider이 공급하는 데이터에 접근할 수 없고 같은 문맥, 같은 context가 아니라고 본다.

     

     


    문법

    // Context 생성
    const MyContent = React.createContext(defaultValue);
    
    // Context Provider을 통한 데이터 공급 (value값이 내부의 자식 컴포넌트들에게 전달됨)
    <MyContext.Provider value= {전역으로 전달하고자하는 값}>
    {/*Context 안에 위치할 자식 컴포넌트들*/}
    </MyContext.Provider>

     

     

    내가 짠 코드에 적용시켜서 실제로 어떻게 쓰이는지 확인해봤다.

     

    Context는 반드시 export 해줘야한다. 그래야 다른 컴포넌트에서도 prop으로 받지 않고도 사용할 수 있다.

    참고로 export는 한 파일에 한번만 쓸 수 있는거라고 잘못 알고 있는 사람들이 있는데, 그렇지 않다. 

    export default는 한번만 쓸 수 있지만 export는 여러번 쓸 수 있다. 자세한건 모듈 시스템에 대해 알아야한다. 우선 context에 대해 얘기한 후 페이지 맨 아래에서 다루겠다.

    export const DiaryStateContext = React.createContext();
    
    fucntion App(){
    	const [data, setData] =  useState("");
        
        ...
        
        return(
        	<DiaryStateContext.Provider value={data}>
            	<div className="App">
                	<DiaryEditor/>
                    <DiaryList/>
                </div>
            </DiaryStateContext.Provider>
        )
    }

     

     

    코드를 다음과 같이 작성한 후 개발자 도구를 열었다.

     

    App 컴포넌트의 하위 컴포넌트인 DiaryEditor, DiaryList가 Context.Provider로 묶여있는 것을 확인할 수 있었다.

    그리고 props로 전달되는 value 역시 data값이 잘 들어간걸 확인할 수 있었다.

    이제 App에서만 쓸 수 있었던 data를 DiaryEditor과 DiaryList에서도 쓸 수 있다!!

     

    이제 하위 컴포넌트인 DiaryList에서 App에 있는 data를 받아와보자.

    useContext와 해당 데이터를 반드시 import 해주어야한다.

     

     


    이전 코드에서 바뀐 부분

    1. App 컴포넌트에서 DiaryList에 diaryList={data}로 data를 전달하는 코드가 필요 없어짐

    2. DiaryList에서 props로 diaryList를 전달받아 사용하던 코드가 필요 없어짐

    import { useContext } from 'react';
    import { DiaryStateContext } from './App';
    
    const DiaryList = () => {
        const diaryList = useContext(DiaryStateContext);
    
        return (
            <div className="DiaryList">
                <h2>일기 리스트</h2>
                <h4>{diaryList.length}개의 일기가 있습니다</h4>
                <div>
                    {diaryList.map((it) => (
                        <DidaryItem key={it.id} {...it} onRemove={onRemove} onEdit={onEdit} />
                    ))}
                </div>
            </div>
        );
    };

     

     

    정상적으로 DiaryList에 모든 데이터가 잘 전달된걸 확인했다.

     

     

    가독성도 좋고 훨씬 편해졌다.

    오.. 그러면 DiaryStateContext.Provider에 함수도 넣고 이것저것 다 넣어버리면 되는거 아닌가?

    그러면 절대 안된다. Provider도 컴포넌트이기 때문이다.

     

    즉, prop이 변경되면 재생성된다. Provider 컴포넌가 재생성되면 자식 컴포넌트들도 전부 재생성된다.

    그렇기 때문에 data 변경과 관련없는 함수들까지 모두 Provider 컴포넌트의 value로 넣어버리면 data가 변경될 때 마다 불필요하게 자식 컴포넌트들이 리렌더링 될 것이다. 만약 함수들을 최적화 시켜놨다면 무용지물이 되어버리는 것이다.

     

    그러면 이런 data를 변경시키는 함수를 useContext로 뽑아쓰고 싶을 땐 어떻게 해야할까? Context를 여러개 쓰면 된다. DiaryStateContext는 오직 data만 공급하는 context이고, 함수는 또 따로 context를 만들어주면 된다. 

     

    export const DiaryDispatch = React.createContext();
    
    function App(){
    	...
         const memoizedDispatches = useMemo(() => {
            return { onCreate, onRemove, onEdit };
        });
        
        ...
        
       return (
            <DiaryStateContext.Provider value={data}>
                <DiaryDispatch.Provider value={memoizedDispatches}>
                    <div className="App">
                       ...
                    </div>
                </DiaryDispatch.Provider>
            </DiaryStateContext.Provider>
        );
    
    }

     

    onCreate, onRemove, onEdit 함수는 최적화를 시켜서 리렌더링 되지 않게 만들어놨고, 따라서 DiaryDispatch.Provider 컴포넌트는 재생성되지 않는다.

     

    함수를 하나로 묶을 때 단순히 const dispatches={onCreate, onRemove...} 이렇게 하지 않은 이유는 저렇게 쓰면 App이 리렌더링될 때 dispatches도 리렌더링 되기 때문이다. 따라서 useMemo를 사용해서 재생성이 되지 않도록 했다.

     

    이제 하위 컴포넌트인 DiaryEditor 컴포넌트에서 각 함수를 뽑아서 써보자.

    onCreate를 중괄호로 묶은 이유는 DiaryDispatchContext가 여러 함수를 묶은 객체이기 때문에 비구조화 할당을 해야하기 때문이다.

    const DiaryEditor=()=>{
    	const { onCreate } = useContext(DiaryDispatchContext);
        ....
    
    }

     

     

     


    export를 여러번 쓸 수 있는 이유

    context API 정리는 여기서 끝이다. 이제 위에서 언급했던 export를 여러번 쓸 수 있는 이유에 대해 정리해보려고 한다.

    import React, { useRef, useEffect, useMemo, useCallback, useReducer } from 'react';

    improt를 할 때 hooks는 무조건 중괄호로 감싸야한다. 왜 그럴까? 모듈 시스템을 알아야한다.

    react라는 파일에서 default import할 수 있는 애는 export default가 된 요소만 할 수 있고 export const같은 애들은 비구조화 할당을 통해 import를 받을 수 있다.

    따라서 App.js에서는 기본적으로 App컴포넌트를 내보내고 있고 부가적으로 DiaryStateContext같은 애들을 내보내고 있다.