React

[React] useContext + Context API

개발조각 2023. 9. 15. 22:19
728x90
반응형

useContext 개념설명


리액트로 만들어진 App은 여러 개의 컴포넌트로 이루어져 있다. 

최상위 App 컴포넌트를 시작해서 트리형태로 쭉쭉 뻗어 나고 있으며

리액트에서 일반적인 데이터 흐름은 위에서 아래로 즉 부모컴포넌트에서 자식컴포넌트 props를 통해 전달된다. 

 

App
<Header usesr={user} />

Header
<SearchBar user={user}>

props를 통해 데이터를 전달할때 부모컴포넌트가 자식 컴포넌트 태그에 넣어주어 단계별로 전달해 주어야 된다.

 

 

엄청 큰 리액트 App이 있다고 가정하면 (컴포넌트도 많고 트리도 깊다)

App내부에 수많은 컴포넌트들이 공통적으로 필요한 전역적인 데이터가 있을 수 있다.

(예를들면 현재 로그인된 사용자 정보, 테마, 언어등)

Global Data: User, Theme, Language 

이런 전역적인 데이터를 props로 일일히 단계적으로 전달하게 된다면 개발자들이 너무 고통스러워 할 것이다.

왜냐하면 코드도 복잡해지고 무언가 하나가 바뀌면 컴포넌트들을 다 찾아다니면서 수정해야되는 현상이 발생하기 때문이다.

 

 

리액트는 이러한 문제점을 간편하게 해결해주는 Context API를 제공해준다.

Context는 App컴포넌트 안에서 전역적으로 사용되는 데이터를 여러 컴포넌트끼리 쉽게 공유할 수 있는 방법을 제공해준다.

Context를 사용하면 props 데이터를 일일히 전달하지 않아도

해당 데이터를 갖고 있는 상위 컴포넌트가 "이 데이터 필요한 사람"하고 하위 컴포넌트들에게 방송을 해줄 수 있다.

하위 컴포넌트는 트리안에 어디에 위치하든 상관없이 "이 데이터 쓸래"라고만 해주면 해당값에 접근할 수 있다.

그렇기 때문에 사용자 정보, 테마, 앱에서 사용되는 언어와 같은 수 많은 컴포넌트들이 필요한 전역적인 데이터를 전달하기에 굉장히 편리하다.

 

 

만약 App컴포넌트에 있는 Data를 C, E컴포넌트에 전달하려면 어떻게 해야될까?

당연히 props를 통해서 모든 중간 컴포넌트를 거쳐서 전달을 해야된다.

이러한 과정을 Prop Drilling이라고 한다.

 

이렇게 하면 어떤문제가 발생될까?

가장 밑에 있는 C, E컴포넌트만 데이터가 필요한데 전달하는 과정에서 데이터는 모든 중간 컴포넌트를 거쳐가야 된다.

여기서는 A. B, D 컴포넌트는 해당데이터를 몰라도 되는데 굳이 받아서 자식 컴포넌트(C, E)에게 전달해야 된다.

이렇게 되면 컴포넌트들이 받는 props도 많아지고 코드도 더러워 진다.

그리고 만약 전달하는 과정에서 실수로 이상한 데이터를 전달하거나 데이터를 수정해버리면

컴포넌트를 찾아다니면서 에러를 수정해야 된다.

 

props대신 context를 사용해서 데이터를 공유하면

한곳에 있는 데이터를 모든 자식컴포넌트들에게 "나 필요한 사람"하고 방송을 할 수 있다.

해당 데이터를 사용하고 싶은 컴포넌트들은 useContext훅을 사용해서 데이터를 받아오기만 하면 된다.

useContext는 context로 공유한 데이터를 쉽게 받아올 수 있게 도와주는 역할을 해준다.

 

context가 좋으며 props가 필요없는거 아닌가?

Context는 꼭 필요할때만!
- Context를 사용하면 컴포넌트를 재사용하기 어려워 질 수 있다.
- Prop Drilling을 피하기 위한 목적이라면 Componenet Componsition (컴포넌트 합성)을 먼저 고려해보자

Context의 주된 목적은 다양한 레벨에 있는 많은 컴포넌트들에게전역적인 데이터를 전달하기 위한 것이다.

리액트 공식 문서에는 Context를 사용한 이유가 단순히 prop Drilling을 피하기 위한 목적이라면 Component Componsition이 더 단순한 해결책이라고 제시하고 있다.

 

예제 1 (테마)


context를 사용하지 않고 state와 prop만 사용해서 만든 예제

import { useState, useEffect, useRef } from "react";
import "./context.css";

function App() {
    return (
        <>
            <UseContext1 />
        </>
    );
}

// dep1
const UseContext1 = () => {
    const [isDark, setIsDark] = useState(false);
    return <Page isDark={isDark} setIsDark={setIsDark}></Page>;
};

const Page = ({ isDark, setIsDark }) => {
    return (
        <div className="page">
            <Header isDark={isDark} />
            <Content isDark={isDark} />
            <Footer isDark={isDark} setIsDark={setIsDark} />
        </div>
    );
};

// dep2
const Header = ({ isDark }) => {
    return (
        <header
            className="header"
            style={{
                backgroundColor: isDark ? "black" : "lightgray",
                color: isDark ? "white" : "black",
            }}
        >
            <h1>welcome 홍길동!</h1>
        </header>
    );
};
const Content = ({ isDark }) => {
    return (
        <div
            className="content"
            style={{
                backgroundColor: isDark ? "black" : "white",
                color: isDark ? "white" : "black",
            }}
        >
            <p>홍길동님, 좋은 하루 되세요</p>
        </div>
    );
};
const Footer = ({ isDark, setIsDark }) => {
    const toggleTheme = () => {
        setIsDark(!isDark);
    };
    return (
        <footer
            className="footer"
            style={{
                backgroundColor: isDark ? "black" : "lightgray",
            }}
        >
            <button className="button" onClick={toggleTheme}>
                Dark Mode
            </button>
        </footer>
    );
};

 

Context를 사용해서 예제를 개선해보자! 

isDark는 전체적인 테마의 정보에 대해 담고 있기 때문에 전역적이라 할 수 있다.

 

isDark의 데이터 플로우를 보면

  • App컴포넌트
    • Page컴포넌트에 props전달
  • Page컴포넌트
    • 실질적으로 isDark를 사용하고 있지 않다. 단지 자녀 컴포넌트들에게 그대로 전달하고 있다.
    • Page컴포는트는 isDark정보를 필요로 하지 않는 중간 컴포넌트라고 할 수 있다.

여기서는 중간 컴포넌트가 1개이지만 10개 20개가되면 심각한 상황이라는 생각이 들게 될 것이다.

 

context를 사용해서 App컴포넌트가 가지고 있는 isDark라는 데이터를 모든 항목에 props를 사용하지 않고 공유를 해보자

// ThemeContext.js
import { createContext } from "react";

export const ThemeContext = createContext(null);
import { ThemeContext } from "./context/ThemeContext";

const UseContext1 = () => {
    const [isDark, setIsDark] = useState(false);

    return (
        <ThemeContext.Provider value={{isDark, setIsDark}}>
            {/* value안에는 전달하고자 하는 데이터를 넣어주면 된다. */}
            <Page />
        </ThemeContext.Provider>
    );
};

value안에는 전달하고자 하는 데이터를 넣어주면 된다.

ThemeContext.provider가 감싸고 있는 모든 하위 컴포넌트는 value에 담겨져 있는 isDark, setIsDark에 접근할 수 있게 된다.

(물론 props를 사용하지 않고)

 

const Page = () => {
    const data = useContext(ThemeContext);
    console.log(data); // {isDark: false, setIsDark: ƒ}
    return (
        <div className="page">
            <Header />
            <Content />
            <Footer />
        </div>
    );
};

중간 컴포넌트임으로 props 다 제거

 

const Header = () => {
    const { isDark } = useContext(ThemeContext);
    console.log(isDark); // false
    return (
        <header
            className="header"
            style={{
                backgroundColor: isDark ? "black" : "lightgray",
                color: isDark ? "white" : "black",
            }}
        >
            <h1>welcome 홍길동!</h1>
        </header>
    );
};

필요한 데이터는 useContext를 사용해서 받기

 

최종코드

// ThemeContext.js
import { createContext } from "react";

export const ThemeContext = createContext(null);
import { useState, useEffect, useRef, useContext } from "react";
import "./context.css";
import { ThemeContext } from "./context/ThemeContext";

function App() {
    return (
        <>
            <UseContext1 />
        </>
    );
}

const UseContext1 = () => {
    const [isDark, setIsDark] = useState(false);

    return (
        <ThemeContext.Provider value={{ isDark, setIsDark }}>
            {/* value안에는 전달하고자 하는 데이터를 넣어주면 된다. */}
            <Page />
        </ThemeContext.Provider>
    );
};

const Page = () => {
    return (
        <div className="page">
            <Header />
            <Content />
            <Footer />
        </div>
    );
};

const Header = () => {
    const { isDark } = useContext(ThemeContext);

    return (
        <header
            className="header"
            style={{
                backgroundColor: isDark ? "black" : "lightgray",
                color: isDark ? "white" : "black",
            }}
        >
            <h1>welcome 홍길동!</h1>
        </header>
    );
};
const Content = () => {
    const { isDark } = useContext(ThemeContext);

    return (
        <div
            className="content"
            style={{
                backgroundColor: isDark ? "black" : "white",
                color: isDark ? "white" : "black",
            }}
        >
            <p>홍길동님, 좋은 하루 되세요</p>
        </div>
    );
};
const Footer = () => {
    const { isDark, setIsDark } = useContext(ThemeContext);

    const toggleTheme = () => {
        setIsDark(!isDark);
    };
    return (
        <footer
            className="footer"
            style={{
                backgroundColor: isDark ? "black" : "lightgray",
            }}
        >
            <button className="button" onClick={toggleTheme}>
                Dark Mode
            </button>
        </footer>
    );
};

export default App;

UseContext1 컴포넌트에서 context를 불러와서 하위컴포넌트들에게

"나 필요한 사람 "이라 방송을 하고

Page 컴포넌트는 정보가 필요하지 않기 때문에 정보(isDark, setIsDark)를 받지 않았고

그 하위 컴포넌트들(Header, Content, Footer)은 정보가 필요하기 때문에 useContext를 이용해서 정보를 사용해주었다.

props를 사용하지 않고 정보를 받아올 수 있었던 것이다.

 

createContext의 인자로 받아온 초기값에 대해 알아보자

// ThemeContext.js
import { createContext } from "react";
export const ThemeContext = createContext("hello");
import { useState, useEffect, useRef, useContext } from "react";
import "./context.css";
import { ThemeContext } from "./context/ThemeContext";

function App() {
    return (
        <>
            <UseContext1 />
        </>
    );
}

const UseContext1 = () => {
    const [isDark, setIsDark] = useState(false);

    return (
    	// <ThemeContext.Provider value={{ isDark, setIsDark }}>
        <Page />
        // </ThemeContext.Provider>
    );
};

const Page = () => {
    const data = useContext(ThemeContext);
    console.log(data);
    return (
        <div className="page"></div>
    );
};

export default App;

초기값이 출력된다.

useContext로 ThemeContext에 대한 정보를 받아왔는데 상위에서 해당 컴포넌트에 Provider로 감싸주지 않았다면

(vallue가 없는거임)

ThemeContext에 createContext의 인자로 넘겨준 초기값을 받아오게 되는 것이다.

지금은 value를 사용해서 값을 넘겨주기 때문에 초기값이 필요하지 않아서 createContext(null)로 해둔거다.

 

추가 예제 (+ 사용자 정보)


// UserContext.js
import { createContext } from "react";
export const UserContext = createContext(null);
import { useState, useEffect, useRef, useContext } from "react";
import "./context.css";
import { ThemeContext } from "./context/ThemeContext";
import { UserContext } from "./context/UserContext";

function App() {
    return (
        <>
            <UseContext1 />
        </>
    );
}

const UseContext1 = () => {
    const [isDark, setIsDark] = useState(false);

    return (
        <UserContext.Provider value={"사용자"}>
            <ThemeContext.Provider value={{ isDark, setIsDark }}>
                <Page />
            </ThemeContext.Provider>
        </UserContext.Provider>
    );
};

const Page = () => {
    const data = useContext(ThemeContext);
    console.log(data);
    return (
        <div className="page">
            <Header />
            <Content />
            <Footer />
        </div>
    );
};

const Header = () => {
    const { isDark } = useContext(ThemeContext);
    const user = useContext(UserContext);

    return (
        <header
            className="header"
            style={{
                backgroundColor: isDark ? "black" : "lightgray",
                color: isDark ? "white" : "black",
            }}
        >
            <h1>welcome {user}!</h1>
        </header>
    );
};
const Content = () => {
    const { isDark } = useContext(ThemeContext);
    const user = useContext(UserContext);

    return (
        <div
            className="content"
            style={{
                backgroundColor: isDark ? "black" : "white",
                color: isDark ? "white" : "black",
            }}
        >
            <p>{user}님, 좋은 하루 되세요</p>
        </div>
    );
};
const Footer = () => {
    const { isDark, setIsDark } = useContext(ThemeContext);

    const toggleTheme = () => {
        setIsDark(!isDark);
    };
    return (
        <footer
            className="footer"
            style={{
                backgroundColor: isDark ? "black" : "lightgray",
            }}
        >
            <button className="button" onClick={toggleTheme}>
                Dark Mode
            </button>
        </footer>
    );
};

export default App;

 

 이 글은 별코딩 리액트 훅 강의를 보고 정리한 글입니다.

728x90
반응형