091

[JS/React] Shared State, Context 본문

Programming Language/JavaScript&TypeScript

[JS/React] Shared State, Context

공구일 2026. 6. 6. 22:32
728x90

1. Shared State

 

- Shared State는 하나의 데이터를 여러 컴포넌트에서 함께 써야할 때, 각자 state를 따로 갖지 않고 공통 부모 컴포넌트의 state를 공유하는 방식입니다. 이런 방식이 필요한 이유는 각 컴포넌트가 state를 따로 가지면 데이터가 달라질 수 있어 동기화 문제가 생기기 때문에 부모가 state를 가지고 자식들에게 props를 내려주면 항상 같은 데이터를 공유합니다.

import React, { useState } from 'react';

function DoubleDisplay({ value }) {
    return (
        <div>
            <span>자식 A(x2) : {value * 2}
            </span>
        </div>
    );
}

function TripleDisplay({ value }) {
    return (
        <div>
            <span>자식 B(x3) : {value * 3}
            </span>
        </div>
    );
}
export default function SharedState() {
    const [value, setValue] = useState(0);
    const handleChange = (e) => {
        setValue(Number(e.target.value));
    };
    return (
        <div>
            부모 컴포넌트
            <input
                type="number"
                value={value}
                onChange={handleChange}
            />
            <DoubleDisplay value={value} />
            <TripleDisplay value={value} />
        </div>
    );
}

-> 자식한테 사용할 state만을 props로 넘기고 setState를 onChange를 통해 state값을 변경하고 값이 변경됐을 때 컴포넌트 전체가 리렌더링됩니다.

 

- 반대로 자식 컴포넌트에서 부모의 state를 변경해야할 때 쓰는 패턴은 Lifting State Up으로, props는 읽기 전용이라 자식이 직접 수정할 수 없고, 부모의 핸들러 함수를 props로 전달해서 자식이 호출하는 방식을 사용합니다.

import React, { useState } from 'react';

function ComponentC({ temp, changeTemp }) {
    return (
        <div>
            <span>단위(섭씨) : </span>
            <input
                type="number"
                value={temp}
                onChange={(e) => changeTemp(e.target.value)}
            />
        </div>
    );
}

function ComponentF({ temp, changeTemp }) {
    return (
        <div>
            <span>단위(화씨) : </span>
            <input
                type="number"
                value={temp}
                onChange={(e) => changeTemp(e.target.value)}
            />
        </div>
    );
}


export default function Calculator() {
    const [temperature, setTemperature] = useState(0);
    const [type, setType] = useState('c');
    const toFahrenheit = (celsius) => { // 섭씨->화씨
        return ((celsius * 9) / 5 + 32).toFixed(1);
    }
    const toCelsius = (fahrenheit) => { // 화씨->섭씨
        return (((fahrenheit - 32) * 5) / 9).toFixed(1);
    }
    const handleCelsiusChange = (temp) => {
        setTemperature(temp);
        setType('c');
    };
    const handleFahrenheitChange = (temp) => {
        setTemperature(temp);
        setType('f');
    };
    const celsius = type == "c" ?
        temperature : toCelsius(temperature);
    const fahrenheit = type == "f" ?
        temperature : toFahrenheit(temperature);
    return (
        <div>
            <h2>온도 변환</h2>
            <ComponentC temp={celsius}
                changeTemp={handleCelsiusChange} />
            <ComponentF temp={fahrenheit}
                changeTemp={handleFahrenheitChange} />
        </div>
    );
}

-> setState를 통해 재랜더링되면 일반 변수인 celsius와 fahrenheit가 변합니다.

-> 여기서 왜 celsius와 fahrenheit 두개의 상태로 두지 않은 이유는 React의 Single Source of Truth에 따라 한 변수로 통합해서 작성한 것입니다. 그리고 React에서는 계상 가능한 값은 state로 만들지 않는 것을 권장합니다. 만약 특정값에 영향을 받아 또다시 계산을 통해 출력해야하는 값을 만들고 싶다면 이 역시 변수로 만들면 됩니다.

const result =
        Number(celsius) >= 100
            ? '물이 끓습니다.'
            : '물이 끓지 않습니다.';
//return의 <div> </div> 내부에서
<p>{result}</p>

 

- 위의 두가지 중 무엇을 사용하던지 부모가 사용할 컴포넌트까지 드릴로 구멍을 뚫듯이 props를 사용하지 않는 하위 컴포넌트에까지 작성하여 통과시켜야하는 문제를 Props Drilling이라고 합니다. 이 문제를 해결하기 위해 Context가 등장했습니다.

 

2. Context

 

- Context란 React 컴포넌트 트리 안에서 전역적으로 데이터를 공유하는 방법으로, 중간 컴포넌트를 거치치 않고 필요한 컴포넌트가 직접 데이터에 접근합니다. 사용자의 로그인 여부, 로그인 정보, UI 테마, 현재 선택된 언어 등과 같이 여러 컴포넌트에서 자주 사용하는 데이터를 Context처리를 합니다.

 

- Context는 컨텍스트 객체 생성, 데이터 제공자, 데이터 구독자로 3개로 이루어집니다.

-> React.createContext는 Context 객체를 생성하기 위한 함수로, Context 객체를 구독하고 있는 컴포넌트를 렌더링할 때 트리 상위에서 가장 가까이에 있는 상위 레벨의 Provider로부터 현재 값을 읽습니다. 

const ThemeContext = React.createContext(); // 기본값 없이 생성
const ThemeContext = React.createContext('light'); // 기본값 있이 생성
// 'light'는 Provider가 없을 때 쓰는 기본값

export default ThemeContext;

-> Provider는 Context 객체에 포함된 React 컴포넌트로 데이터 제공자 역할을 합니다. 이 Provider 하위 컴포넌트들은 해당 context의 데이터에 접근이 가능하며, value prop을 받아서 이 값을 하위 컴포넌트에 접근가능하게 합니다.

<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>

-> Consumer는 context의 데이터를 구독하는 React 컨포넌트이며, 함수 컴포넌트에서 context 구독에 사용합니다. 컴포넌트의 자식은 함수로 사용되는데 그 이유는 현재 context의 value를 받아서 React 노드를 반환하기 때문에 내부 값을 전달하며 사용합니다.

//children이 JSX
<MyComponent>
  <div>안녕</div>
</MyComponent>

//function as a child
<MyComponent>
  {(value) => <div>{value}</div>}
</MyComponent>

<MyComponent children={value => <div>{value}</div>} />

• 이러한 컴포넌트 하위에 함수를 방식을 fucntion as a child라고 합니다.

-> 이런 구독을 useContext로 쓸 수 있으면 이전 버전인 Consumer에서는 JSX 안에 함수를 써야하기 때문에 코드가 한 단계 깊어지는 불편함이 있었지만, useContext를 사용하면 컴포넌트 상단에서 한줄로 꺼내쓸 수 있어 편리합니다.

// Consumer (구버전)
function ThemedButton() {
  return (
    <ThemeContext.Consumer>
      {({ theme, toggleTheme }) => (
        <button onClick={toggleTheme}>
          {theme}
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

// useContext (현재)
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  return (
    <button onClick={toggleTheme}>
      {theme}
    </button>
  );
}

=> 어떤 방식으로든 데이터를 꺼내쓸 때 상위 컴포넌트에 Provider가 없을 경우에는 생성시에 넣어둔 기본 값을 사용하고, Provider가 여러겹 쌓여있다면 가장 가까운 값을 사용합니다.

 

- 실습: 테마변경+언어 변경

import React, { useContext, useState, useCallback } from 'react';

const ThemeContext = React.createContext();
ThemeContext.displayName = 'Theme-Context';

const LanguageContext = React.createContext();
LanguageContext.displayName = 'Language-Context';

function MainContent(props){
    const {theme, toggleTheme } = useContext(ThemeContext);
    const { language, toggleLanguage } = useContext(LanguageContext);

    const result = language === 'kor' ? '안녕하세요' : 'Hello';

    return(
        <div
        style={{
            width:'100vw',
            height:'100vh',
            padding: '1.5rem',
            backgroundColor: theme === 'light' ? 'white' : 'black',
            color: theme === 'light' ? 'black' : 'white',
        }}>
            <p> 테마 변경이 가능한 웹사이트입니다. </p>
            <button onClick={toggleTheme}>테마변경</button>

            <br/>
            언어:
            <select id='language' onChange={(e) => toggleLanguage(e.target.value)}>
                <option value='kor'>한글</option>
                <option value='eng'>영어</option>
            </select>

            <p>{result}</p>
        </div>
    );
}

export default function ThemeAndLanguage(props){
    const [theme, setTheme] = useState('light');
    const [language, setLanguage] = useState('kor');

    const toggleTheme = useCallback(()=>{
        if(theme === 'light'){
            setTheme('dark')
        } else if(theme === 'dark'){
            setTheme('light')
        }
    },[theme]); //theme 값을 일어야함(의존성 필요)

    const toggleLanguage = useCallback((lang)=>{
        setLanguage(lang);
    },[]); //lang을 인자로 넘겨줌(의존성 필요없음)

    return(
        <ThemeContext.Provider value={{ theme, toggleTheme}}>
            <LanguageContext.Provider value={{ language, toggleLanguage}}>
                <MainContent />
            </LanguageContext.Provider>
        </ThemeContext.Provider>
    )
}

 

-> toggleTheme을 useCallback으로 감싸지 않고 Context value로 전달하면 렌더링마다 함수가 새로 생성되어 불필요한 렌더링이 발생하여 의존성 배열 값이 변할 때만 리렌더링하게 설정해둔 것입니다.

728x90