8. Context 전달하기

2024. 6. 12. 05:22리액트 (React)/리액트 레시피 (React Recipt)


Theme

 

7. State 보존하기 글에서 만들었던 친구 목록에 테마를 추가해봅니다.

 

 

이는 오른쪽 상단의 테마 버튼을 눌러서 전환할 수 있도록 합니다.

 

먼저 List에 theme state를 추가하고 div의 className에 적용합니다.

export default function List(...) {
    const [theme, setTheme] = useState<Theme>("light");
    
    return (
        <div className="layout">
            <div className={`box ${theme}`}>  // theme에 따라 className이 변경
                <Header />
                {names.map((name) => (
                    <div className="list-item" onClick={() => onClickListItem(name)}>
                        <span className="name">{name}</span>
                    </div>
                ))}
            </div>
        </div>
    );
}

 

그리고 Header에도 테마를 적용할 뿐만 아니라,

내부 버튼으로 테마를 제어할 계획이므로 props 및 관련 로직을 추가해줍니다.

import "./Header.scss";
import { Theme } from "./List";

export default function Header({
    theme,
    onClickTheme,
}: {
    theme: Theme;
    onClickTheme: () => void;
}) {
    function renderIcon(theme: Theme) {
        if (theme === "light") {
            return <i className="fa-solid fa-sun" />;
        }
        return <i className="fa-solid fa-moon" />;
    }

    return (
        <div className={`friend-header ${theme}`}>
            <span className="title">Chat Friends</span>
            <button className="theme" onClick={() => onClickTheme()}>
                {renderIcon(theme)}
            </button>
        </div>
    );
}

 

결과는 다음과 같습니다.

 

이제 친구 목록 뿐만 아니라 채팅창에도 마찬가지로 적용하려고 합니다.

따라서 theme을 Main으로 옮겨옵니다.

export default function Main() {
    const [theme, setTheme] = useState<Theme>("light");
	
    ...
}

 

그러나 props로 모든 컴포넌트들에게 theme을 넘겨주어야 하니, 고난의 작업이 예상될 것으로 보이네요.

특히 컴포넌트를 만들 때도 조금 더러워지는 걸 생각해야됩니다.

<div>
    {selectedName !== "" ? (
        <>
            {previousName && (
                <ChangeButton
                    name={previousName}
                    direction="left"
                    onClick={() => setSelectedName(previousName)}
                    theme={theme} // give theme
                />
            )}
            <Box
                key={selectedName}
                name={selectedName}
                onClickList={() => setSelectedName("")}
                theme={theme} // give theme
            />
            {nextName && (
                <ChangeButton
                    name={nextName}
                    direction="right"
                    onClick={() => setSelectedName(nextName)}
                    theme={theme} // give theme
                />
            )}
        </>
    ) : (
        <List
            names={friendNames}
            onClickListItem={(name) => {
                setSelectedName(name);
            }}
            onClickTheme={() => handleClickTheme()}
            theme={theme} // give theme
        />
    )}
</div>

 

 

 

 

 

 

 

 


Context

 

context를 사용하면 props으로 전달하지 않아도, 모든 컴포넌트가 각자 사용할 수 있도록 합니다.

가령 테마(theme)를 모든 컴포넌트가 사용해서 각자 본인의 색상을 바꾼다고 하면,

context를 사용하면 모든 컴포넌트가 props로 theme을 만들지 않아도 되죠.

 

context를 사용하는 방법은 다음과 같습니다.

 

1. context를 만듭니다. 

import { createContext } from "react";


export type Theme = "light" | "dark";

export const ThemeContext = createContext<Theme>("light");

 

2. 만든 context로 Provider를 생성합니다.

import { Theme, ThemeContext } from "./ThemeContext";
...

export default function Main() {
    const [theme, setTheme] = useState<Theme>("light");
    
    ...

    return (
        <div>
            <ThemeContext.Provider value={theme}>
                {...} // now, every components can use theme!
            </ThemeContext.Provider>
        </div>
    );
}

 

3. Provider 내부의 컴포넌트들은 useContext 훅을 사용해 theme을 사용할 수 있습니다.

const theme = useContext(ThemeContext);

 

예를 들어 Header는 다음과 같이 context을 사용합니다.

import { useContext } from "react";
import { Theme, ThemeContext } from "../ThemeContext";
import "./Header.scss";

export default function Header({ onClickTheme }: { onClickTheme: () => void }) {
    const theme = useContext(ThemeContext);

    function renderIcon(theme: Theme) {
        if (theme === "light") {
            return <i className="fa-solid fa-sun" />;
        }
        return <i className="fa-solid fa-moon" />;
    }

    return (
        <div className={`friend-header ${theme}`}>
            <span className="title">Chat Friends</span>
            <button className="theme" onClick={() => onClickTheme()}>
                {renderIcon(theme)}
            </button>
        </div>
    );
}

 

 

 

이렇게 context를 이용해 만든 결과는 다음과 같습니다.

 

 

 

 

 

 


Alternative

 

context는 props를 만들 필요 없이 컴포넌트 내에서 알아서 사용할 수 있으니 매우 편리하죠.

하지만 일반적으로는 props를 통해 명시적으로 노출해주는 것이 좋습니다.

 

일단 context는 보이지 않기 때문에, 이미 context로 정의되어 있는지 아닌 지 확인하기가 어렵습니다.

따라서 일일히 context가 정의되어 있는 곳을 찾아야 할 뿐 아니라 Provider가 설정되어 있는 지도 확인해야 하죠.

이런 불편함 뿐 아니라, 잠깐 놓쳐도 중복으로 props와 context를 만들 수 있습니다.

특히 context를 사용하는 건 컴포넌트 자체의 선택이기 때문에, 컴포넌트를 사용하는 입장에서 해당 정보가 필수인 지 모릅니다.

 

따라서 올바르게 context를 사용하려면 왜 context를 사용하는 것이 더 좋은 지 설명할 수 있어야 합니다.

  • 사전에 모두 약속하고 정했습니다.
  • props로 표현하기에는 가독성과 유지보수가 더 떨어집니다.
  • 대다수의 하위 컴포넌트가 해당 정보를 사용합니다.
  • ...

그렇게만 한다면 context는 충분히 좋은 대안이 될 수 있죠!

 

 

 

 

 

 

 


References

 

 

Context를 사용해 데이터를 깊게 전달하기 – React

The library for web and native user interfaces

ko.react.dev

 

'리액트 (React) > 리액트 레시피 (React Recipt)' 카테고리의 다른 글

9. Ref로 제어하기  (0) 2024.06.12
7. State 보존하기  (0) 2024.06.11
6. 인터렉티브한 Input 만들기  (0) 2024.06.10
5. State로 리렌더링하기  (0) 2024.06.10
4. CSS 입히기  (0) 2024.06.10