QnA

compound-components

Q&A 정리: compound-components

Compound Components 패턴이란 무엇이며, 어떤 문제를 해결하는가?

두 개 이상의 컴포넌트가 짝을 이뤄 함께 동작하는 패턴이다. HTML의 <select><option>처럼, 따로 쓰면 의미가 없지만 함께 쓰면 유연하고 읽기 쉬운 구조를 만든다.

The idea is that you have two or more components that work together to accomplish a useful task. Typically one component is the parent, and the other is the child. The objective is to provide a more expressive and flexible API.

Think of it like <select> and <option>:

<select>
    <option value="value1">key1</option>
    <option value="value2">key2</option>
    <option value="value3">key3</option>
</select>

If you were to try and use one without the other it wouldn't work (or make sense). Additionally it's actually a really great API. Let's check out what it would look like if we didn't have a compound components API to work with (remember, this is HTML, not JSX):

<select options="key1:value1;key2:value2;key3:value3"></select>

And how would you express the disabled attribute with this kind of API? It's kinda madness.

So the compound components API gives you a nice way to express relationships between components.


Compound Components에서 "implicit state"란 무엇이며, 왜 중요한가?

부모 컴포넌트가 내부적으로 상태를 관리하고 자식에게 자동으로 전달하는 것을 "암묵적 상태"라 한다. <select>가 어떤 옵션이 선택됐는지를 자동으로 추적하듯, 개발자가 직접 상태를 전달하지 않아도 자식이 알아서 올바르게 동작한다.

Another important aspect of this is the concept of "implicit state." The <select> element implicitly stores state about the selected option and shares that with it's children so they know how to render themselves based on that state. But that state sharing is implicit because there's nothing in our HTML code that can even access the state (and it doesn't need to anyway).


React에서 Compound Components의 implicit state를 Context로 구현할 때, 부모 컴포넌트는 어떤 구조로 상태를 공유하는가?

React의 Context를 사용해 부모가 상태 저장소를 만들고, 자식들이 그 저장소에서 필요한 값을 꺼내 쓰는 구조다. 부모가 창고를 열어두면 자식들이 자유롭게 물건을 가져가는 방식이다.

So the way this works is we create a context with React where we store the state and a mechanism for updating the state. Then the <Toggle> component is responsible for providing that context value to the rest of the react tree.

const ToggleContext = React.createContext()
 
function Toggle(props) {
    const [on, setOn] = React.useState(false)
    const toggle = React.useCallback(() => setOn((oldOn) => !oldOn), [])
    const value = React.useMemo(() => ({ on, toggle }), [on])
    return (
        <ToggleContext.Provider value={value}>
            {props.children}
        </ToggleContext.Provider>
    )
}

Compound Components에서 자식 컴포넌트가 Provider 바깥에서 렌더링되면 어떤 문제가 생기며, 이를 어떻게 방어하는가?

자식이 부모 바깥에서 단독으로 사용되면 공유 상태를 찾을 수 없어 오류가 난다. 이를 방지하기 위해 Context 값이 없으면 명확한 에러 메시지를 던져, 개발자가 잘못된 사용 위치를 즉시 알 수 있게 한다.

function useToggleContext() {
    const context = React.useContext(ToggleContext)
    if (!context) {
        throw new Error(
            `Toggle compound components cannot be rendered outside the Toggle component`,
        )
    }
    return context
}

Compound Components에서 Context value에 useMemo를 쓰는 이유는?

부모가 다시 그려질 때마다 새로운 객체가 만들어지면, 값이 바뀌지 않았는데도 모든 자식이 불필요하게 다시 그려진다. useMemo로 값이 실제로 변할 때만 새 객체를 만들어 불필요한 화면 갱신을 방지한다.

const value = React.useMemo(() => ({ on, toggle }), [on])
return (
    <ToggleContext.Provider value={value}>
        {props.children}
    </ToggleContext.Provider>
)