levi

리바이's Tech Blog

Tech BlogPortfolioBoard
AllActivitiesJavascriptTypeScriptNetworkNext.jsReactWoowacourseAlgorithm
COPYRIGHT ⓒ eunwoo-levi
eunwoo1341@gmail.com

📚 목차

    [React 공식문서] State 관리하기

    ByEunwoo
    2025년 5월 8일
    react

    애플리케이션이 커짐에 따라, state가 어떻게 구성되는지 그리고 데이터가 컴포넌트 간에 어떻게 흐르는지에 대해 의식적으로 파악하면 도움이 된다. 불필요하거나 중복된 state는 버그의 흔한 원인이다.
    이 장에서는 state를 잘 구성하는 방법, state 업데이트 로직을 유지 보수 가능하게 관리하는 방법, 그리고 멀리 있는 컴포넌트 간에 state를 공유하는 방법을 React 공식문서를 통해 알아보자.

    선언형 UI와 명령형 UI 비교

    React는 선언적인 방식으로 UI를 조작한다. 개별적인 UI를 직접 조작하는 것 대신에 컴포넌트 내부에 여러 state를 묘사하고 사용자의 입력에 따라 state를 변경한다.

    UI 상호작용을 디자인할 때 사용자가 액션을 하면 어떻게 UI를 변경 해야 할지 고민해본 적이 있을 것이다. 사용자가 폼을 제출한다고 생각해보자.

    • 폼에 무언가를 입력하면 “제출” 버튼이 활성화될 것이다.
    • ”제출” 버튼을 누르면 폼과 버튼이 비활성화되고 스피너가 나타날 것이다.
    • 네트워크 요청이 성공하면 폼은 숨겨질 것이고 “감사합니다.” 메시지가 나타날 것이다.
    • 네트워크 요청이 실패하면 오류 메시지가 보일 것이고 폼은 다시 활성화될 것이다.

    내용은 명령형 프로그래밍에서 상호작용을 구현하는 방법이다. UI를 조작하기 위해서는 발생한 상황에 따라 정확한 지침을 작성해야만 한다.

    아래의 명령형 UI 프로그래밍 예시는 React 없이 만들어진 폼입니다. 브라우저에 내장된 DOM을 사용하였다.

    async function handleFormSubmit(e) {
      e.preventDefault();
      disable(textarea);
      disable(button);
      show(loadingMessage);
      hide(errorMessage);
      try {
        await submitForm(textarea.value);
        show(successMessage);
        hide(form);
      } catch (err) {
        show(errorMessage);
        errorMessage.textContent = err.message;
      } finally {
        hide(loadingMessage);
        enable(textarea);
        enable(button);
      }
    }
     
    function handleTextareaChange() {
      if (textarea.value.length === 0) {
        disable(button);
      } else {
        enable(button);
      }
    }
     
    function hide(el) {
      el.style.display = 'none';
    }
     
    function show(el) {
      el.style.display = '';
    }
     
    function enable(el) {
      el.disabled = false;
    }
     
    function disable(el) {
      el.disabled = true;
    }
     
    function submitForm(answer) {
      // 네트워크에 접속한다고 가정해보자.
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          if (answer.toLowerCase() === 'istanbul') {
            resolve();
          } else {
            reject(new Error('Good guess but a wrong answer. Try again!'));
          }
        }, 1500);
      });
    }
     
    let form = document.getElementById('form');
    let textarea = document.getElementById('textarea');
    let button = document.getElementById('button');
    let loadingMessage = document.getElementById('loading');
    let errorMessage = document.getElementById('error');
    let successMessage = document.getElementById('success');
    form.onsubmit = handleFormSubmit;
    textarea.oninput = handleTextareaChange;

    위의 예시에서는 문제가 없겠지만 위와 같이 UI를 조작하면 더 복잡한 시스템에서는 난이도가 기하급수적으로 올라간다.
    로운 UI 요소나 새로운 상호작용을 추가하려면 버그의 발생을 막기 위해 기존의 모든 코드를 주의 깊게 살펴봐야만 할 것이다.

    React는 이러한 문제점을 해결하기 위해 만들어졌다. React에서는 직접 UI를 조작할 필요가 없다. 컴포넌트를 직접 활성화하거나 비활성화하거나 보여주거나 숨길 필요가 없다. 대신에 무엇을 보여주고 싶은지 선언하기만 하면 된다.

    UI를 선언적인 방식으로 생각하기

    • 컴포넌트의 다양한 시각적 state를 확인.
    • 무엇이 state 변화를 트리거하는지 알아냄.
    • useState를 사용해서 메모리의 state를 표현.
    • 불필요한 state 변수를 제거.
    • state 설정을 위해 이벤트 핸들러를 연결.
    import { useState } from 'react';
     
    export default function Form() {
      const [answer, setAnswer] = useState('');
      const [error, setError] = useState(null);
      const [status, setStatus] = useState('typing');
     
      if (status === 'success') {
        return <h1>That's right!</h1>;
      }
     
      async function handleSubmit(e) {
        e.preventDefault();
        setStatus('submitting');
        try {
          await submitForm(answer);
          setStatus('success');
        } catch (err) {
          setStatus('typing');
          setError(err);
        }
      }
     
      function handleTextareaChange(e) {
        setAnswer(e.target.value);
      }
     
      return (
        <>
          <h2>City quiz</h2>
          <p>In which city is there a billboard that turns air into drinkable water?</p>
          <form onSubmit={handleSubmit}>
            <textarea
              value={answer}
              onChange={handleTextareaChange}
              disabled={status === 'submitting'}
            />
            <br />
            <button disabled={answer.length === 0 || status === 'submitting'}>Submit</button>
            {error !== null && <p className='Error'>{error.message}</p>}
          </form>
        </>
      );
    }
     
    function submitForm(answer) {
      // 네트워크에 접속한다고 가정해보자.
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          let shouldError = answer.toLowerCase() !== 'lima';
          if (shouldError) {
            reject(new Error('Good guess but a wrong answer. Try again!'));
          } else {
            resolve();
          }
        }, 1500);
      });
    }

    이러한 코드가 기존의 명령형 프로그래밍 예시보다는 길지만 그래도 조금 더 견고하다. 모든 상호작용을 state로 표현하게 되면 이후에 새로운 시각적 state가 추가되더라도 기존의 로직이 손상되는 것을 막을 수 있다. 또한 상호작용 자체의 로직을 변경하지 않고도 각각의 state에 표시되는 항목을 변경할 수 있다.

    State 구조 선택하기

    State를 잘 구조화한다면 지속적인 버그의 원인이 되는 컴포넌트가 아닌, 수정과 디버깅이 용이한 컴포넌트를 만들 수 있다. 가장 중요한 원칙은 state가 중복되거나 불필요한 정보를 포함하지 않는 것이다. 불필요한 state가 있다면 업데이트하는 것을 잊어버려 버그가 발생하기 쉽다.

    State 구조화 원칙

      1. 연관된 state 그룹화하기. 두 개 이상의 state 변수를 항상 동시에 업데이트한다면, 단일 state 변수로 병합하는 것을 고려하자.
      1. State의 모순 피하기. 여러 state 조각이 서로 모순되고 “불일치”할 수 있는 방식으로 state를 구성하는 것은 실수가 발생할 여지를 만든다. 이를 피하자.
      1. 불필요한 state 피하기. 렌더링 중에 컴포넌트의 props나 기존 state 변수에서 일부 정보를 계산할 수 있다면, 컴포넌트의 state에 해당 정보를 넣지 않아야 한다.
      1. State의 중복 피하기. 여러 상태 변수 간 또는 중첩된 객체 내에서 동일한 데이터가 중복될 경우 동기화를 유지하기가 어렵다. 가능하다면 중복을 줄이자.
      1. 깊게 중첩된 state 피하기. 깊게 계층화된 state는 업데이트하기 쉽지 않다. 가능하면 state를 평탄한 방식으로 구성하는 것이 좋다.

    이러한 원칙 뒤에 있는 목표는 오류 없이 상태를 쉽게 업데이트하는 것이다. State에서 불필요하고 중복된 데이터를 제거하면 모든 데이터 조각이 동기화 상태를 유지하는 데 도움이 된다.

    To paraphrase Albert Einstein, “Make your state as simple as it can be—but no simpler.”

    연관된 state 그룹화하기

    단일 state 변수와 다중 state 변수 사이에서 무엇을 사용할지 불확실한 경우가 있다.

    const [x, setX] = useState(0);
    const [y, setY] = useState(0);

    위의 두 개의 state 변수는 연관되어 있다. 두 개의 state 변수가 항상 함께 변경된다면, 단일 state 변수로 통합하는 것이 좋다.

    const [position, setPosition] = useState({ x: 0, y: 0 });

    그러면 마우스 커서를 움직이면 빨간 점의 두 좌표가 모두 업데이트되는 이 예시처럼 항상 동기화를 유지하는 것을 잊지 않을 것이다.

    불필요한 state 피하기

    렌더링 중에 컴포넌트의 props나 기존 state 변수에서 일부 정보를 계산할 수 있다면, 컴포넌트의 state에 해당 정보를 넣지 않아야 한다.

    import { useState } from 'react';
     
    export default function Form() {
      const [firstName, setFirstName] = useState('');
      const [lastName, setLastName] = useState('');
      const [fullName, setFullName] = useState('');
     
      function handleFirstNameChange(e) {
        setFirstName(e.target.value);
        setFullName(e.target.value + ' ' + lastName);
      }
     
      function handleLastNameChange(e) {
        setLastName(e.target.value);
        setFullName(firstName + ' ' + e.target.value);
      }
     
      return (
        <>
          <h2>Let’s check you in</h2>
          <label>
            First name: <input value={firstName} onChange={handleFirstNameChange} />
          </label>
          <label>
            Last name: <input value={lastName} onChange={handleLastNameChange} />
          </label>
          <p>
            Your ticket will be issued to: <b>{fullName}</b>
          </p>
        </>
      );
    }

    이 양식에는 firstName, lastName, fullName의 세 가지 state 변수가 있다. 그러나 fullName은 불필요하다. 렌더링 중에 항상 firstName과 lastName에서 fullName을 계산할 수 있기 때문에 state에서 제거하는게 좋다.

    import { useState } from 'react';
     
    export default function Form() {
      const [firstName, setFirstName] = useState('');
      const [lastName, setLastName] = useState('');
     
      const fullName = firstName + ' ' + lastName;
     
      function handleFirstNameChange(e) {
        setFirstName(e.target.value);
      }
     
      function handleLastNameChange(e) {
        setLastName(e.target.value);
      }
     
      return (
        <>
          <h2>Let’s check you in</h2>
          <label>
            First name: <input value={firstName} onChange={handleFirstNameChange} />
          </label>
          <label>
            Last name: <input value={lastName} onChange={handleLastNameChange} />
          </label>
          <p>
            Your ticket will be issued to: <b>{fullName}</b>
          </p>
        </>
      );
    }

    여기에서, fullName은 state 변수가 아니다. 대신 렌더링 중에 계산된다.

    const fullName = firstName + ' ' + lastName;

    따라서 변경 핸들러는 이를 업데이트하기 위해 특별한 작업을 수행할 필요가 없다. setFirstName 또는 setLastName을 호출하면, 다시 렌더링하는 것을 유발하여, 다음 fullName이 새 데이터로 계산된다.

    컴포넌트 간 State 공유하기

    때때로 두 컴포넌트의 state가 항상 함께 변경되기를 원할 수 있다. 그렇게 하려면 각 컴포넌트에서 state를 제거하고 가장 가까운 공통 부모 컴포넌트로 옮긴 후 props로 전달해야 한다. 이 방법을 State 끌어올리기라고 하며 React 코드를 작성할 때 가장 흔히 하는 일 중 하나이다.

    import { useState } from 'react';
     
    export default function Accordion() {
      const [activeIndex, setActiveIndex] = useState(0);
      return (
        <>
          <h2>Almaty, Kazakhstan</h2>
          <Panel title='About' isActive={activeIndex === 0} onShow={() => setActiveIndex(0)}>
            With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to
            1997, it was its capital city.
          </Panel>
          <Panel title='Etymology' isActive={activeIndex === 1} onShow={() => setActiveIndex(1)}>
            The name comes from <span lang='kk-KZ'>алма</span>, the Kazakh word for "apple" and is often
            translated as "full of apples". In fact, the region surrounding Almaty is thought to be the
            ancestral home of the apple, and the wild <i lang='la'>Malus sieversii</i> is considered a
            likely candidate for the ancestor of the modern domestic apple.
          </Panel>
        </>
      );
    }
     
    function Panel({ title, children, isActive, onShow }) {
      return (
        <section className='panel'>
          <h3>{title}</h3>
          {isActive ? <p>{children}</p> : <button onClick={onShow}>Show</button>}
        </section>
      );
    }

    상태 끌어올리기는 종종 state로 저장하고 있는 것의 특성을 바꾼다. 위의 코드를 보면 상태 끌어올리기가 어떻게 동작하는지 알 수 있다.
    state를 공통 부모 컴포넌트로 옮기는 것은 두 패널을 조정할 수 있게 한다. 두 개의 “보임” 플래그 대신 활성화된 인덱스를 사용하는 것은 한 번에 하나의 패널만 활성화됨을 보장한다. 그리고 자식 컴포넌트로 이벤트 핸들러를 전달하는 것은 자식 컴포넌트에서 부모의 상태를 변경할 수 있게 한다.

    State 끌어올리기

    • Accordion의 activeIndex state가 1로 변경되면 두 번째 Panel은 isActive = true를 받게 된다.

    State를 보존하고 초기화하기

    참고하면 좋은 React 공식문서

    컴포넌트가 리렌더링 될 때, React는 트리에서 유지(및 업데이트) 할 부분과, 버리거나 다시 생성할 부분을 결정해야 한다. 대부분의 경우 React의 자동화된 동작이 충분히 잘 작동한다. 기본적으로 React는 기존에 렌더링 된 컴포넌트 트리와 “일치하는” 트리 부분을 보존한다.

    하지만 때로는 이것이 바람직한 동작이 아닐 수 있다. 아래 채팅 앱에서는 메시지를 입력한 후에 수신자를 변경하더라도 입력이 초기화되지 않는다. 따라서 사용자가 실수로 잘못된 사람에게 메시지를 보낼 수도 있다.

    다른 위치에 컴포넌트를 렌더링하기

    import { useState } from 'react';
     
    export default function Scoreboard() {
      const [isPlayerA, setIsPlayerA] = useState(true);
      return (
        <div>
          {isPlayerA && <Counter person='Taylor' />}
          {!isPlayerA && <Counter person='Sarah' />}
          <button
            onClick={() => {
              setIsPlayerA(!isPlayerA);
            }}
          >
            Next player!
          </button>
        </div>
      );
    }
     
    function Counter({ person }) {
      const [score, setScore] = useState(0);
      const [hover, setHover] = useState(false);
     
      let className = 'counter';
      if (hover) {
        className += ' hover';
      }
     
      return (
        <div
          className={className}
          onPointerEnter={() => setHover(true)}
          onPointerLeave={() => setHover(false)}
        >
          <h1>
            {person}'s score: {score}
          </h1>
          <button onClick={() => setScore(score + 1)}>Add one</button>
        </div>
      );
    }

    처음에는 isPlayerA가 true입니다. 따라서 첫 번째 자리에 Counter가 있고 두 번째 자리는 비어있다.
    ”Next player”를 클릭하면 첫 번째 자리는 비워지고 두 번째 자리에 Counter가 온다.

    State 보존하기

    각 Counter의 state는 DOM에서 지워질 때마다 제거됩니다. 이것이 버튼을 누를 때마다 초기화되는 이유이다.
    만약 2개의 State가 같은 컴포넌트에 있다면, React는 두 개의 Counter를 같은 컴포넌트로 간주하고, 그 중 하나를 지우고 다른 하나를 업데이트한다. 따라서 state가 초기화되지 않고 유지가 될 것이다.

    이 방법은 같은 자리에 적은 수의 독립된 컴포넌트만을 가지고 있을 때 편리하다. 이 예시에서는 두 개밖에 없기 때문에 JSX에서 각각 렌더링하기 번거롭지 않다.

    key를 이용해 state를 초기화하기

    import { useState } from 'react';
    import Chat from './Chat.js';
    import ContactList from './ContactList.js';
     
    export default function Messenger() {
      const [to, setTo] = useState(contacts[0]);
      return (
        <div>
          <ContactList
            contacts={contacts}
            selectedContact={to}
            onSelect={(contact) => setTo(contact)}
          />
          <Chat contact={to} />
        </div>
      );
    }
     
    const contacts = [
      { name: 'Taylor', email: 'taylor@mail.com' },
      { name: 'Alice', email: 'alice@mail.com' },
      { name: 'Bob', email: 'bob@mail.com' },
    ];

    <Chat key={email} /> 처럼 다른 key를 전달함으로써 React의 기본 동작을 무시하고 강제로 컴포넌트의 상태를 초기화할 수 있다. 이를 통해 수신자가 다르다면 새로운 데이터(및 input과 같은 UI)로 처음부터 다시 생성해야 하는 별개의 Chat 컴포넌트로 간주해야 한다는 것을 React에 알려준다. 이제 수신자를 변경하면 같은 컴포넌트를 렌더링하더라도 input 필드가 초기화된다.

    import { useState } from 'react';
    import Chat from './Chat.js';
    import ContactList from './ContactList.js';
     
    export default function Messenger() {
      const [to, setTo] = useState(contacts[0]);
      return (
        <div>
          <ContactList
            contacts={contacts}
            selectedContact={to}
            onSelect={(contact) => setTo(contact)}
          />
          <Chat key={to.email} contact={to} />
        </div>
      );
    }
     
    const contacts = [
      { name: 'Taylor', email: 'taylor@mail.com' },
      { name: 'Alice', email: 'alice@mail.com' },
      { name: 'Bob', email: 'bob@mail.com' },
    ];

    리렌더링할 때 state를 유지하고 싶다면, 트리 구조가 “같아야” 한다. 만약 구조가 다르다면 React가 트리에서 컴포넌트를 지울 때 state로 지우기 때문에 state가 유지되지 않는다.

    주의 - 컴포넌트 함수를 중첩해서 정의하지 않는다.

    아래 예시를 보면 MyComponent 안에서 MyTextField 컴포넌트 함수를 정의하고 있다.

    import { useState } from 'react';
     
    export default function MyComponent() {
      const [counter, setCounter] = useState(0);
     
      function MyTextField() {
        const [text, setText] = useState('');
     
        return <input value={text} onChange={(e) => setText(e.target.value)} />;
      }
     
      return (
        <>
          <MyTextField />
          <button
            onClick={() => {
              setCounter(counter + 1);
            }}
          >
            Clicked {counter} times
          </button>
        </>
      );
    }

    버튼을 누를 때마다 입력 state가 사라진다. 이것은 MyComponent를 렌더링할 때마다 다른 MyTextField 함수가 만들어지기 때문이다. 따라서 같은 함수에서 다른 컴포넌트를 렌더링할 때마다 React는 그 아래의 모든 state를 초기화한다. 이런 문제를 피하려면 항상 컴포넌트를 중첩해서 정의하지 않고 최상위 범위에서 정의해야 한다.

    • key가 전역적으로 유일하지 않다는 것을 기억해야 한다. key는 오직 부모 안에서만 자리를 명시하자.

    State 로직을 reducer로 작성하기

    여러 이벤트 핸들러를 통해 많은 state 업데이트가 이루어지는 컴포넌트는 감당하기 힘들 수 있다. 이 때 컴포넌트 외부에서 reducer라는 단일 함수를 사용하여 모든 state 업데이트 로직을 통합할 수 있다. 이벤트 핸들러는 오로지 사용자의 “action”만을 명시하므로 간결해진다. 각 action에 대한 state 업데이트 방법은 파일 맨 마지막 부분의 reducer 함수에 명시되어 있다.

    예를 들어, 아래의 TaskApp 컴포넌트는 state에 tasks 배열을 보유하고 있으며, 세 가지의 이벤트 핸들러를 사용하여 task를 추가, 제거 및 수정한다.

    import { useReducer } from 'react';
    import AddTask from './AddTask.js';
    import TaskList from './TaskList.js';
     
    export default function TaskApp() {
      const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
     
      function handleAddTask(text) {
        dispatch({
          type: 'added',
          id: nextId++,
          text: text,
        });
      }
     
      function handleChangeTask(task) {
        dispatch({
          type: 'changed',
          task: task,
        });
      }
     
      function handleDeleteTask(taskId) {
        dispatch({
          type: 'deleted',
          id: taskId,
        });
      }
     
      return (
        <>
          <h1>Prague itinerary</h1>
          <AddTask onAddTask={handleAddTask} />
          <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} />
        </>
      );
    }
     
    function tasksReducer(tasks, action) {
      switch (action.type) {
        case 'added': {
          return [
            ...tasks,
            {
              id: action.id,
              text: action.text,
              done: false,
            },
          ];
        }
        case 'changed': {
          return tasks.map((t) => {
            if (t.id === action.task.id) {
              return action.task;
            } else {
              return t;
            }
          });
        }
        case 'deleted': {
          return tasks.filter((t) => t.id !== action.id);
        }
        default: {
          throw Error('Unknown action: ' + action.type);
        }
      }
    }
     
    let nextId = 3;
    const initialTasks = [
      { id: 0, text: 'Visit Kafka Museum', done: true },
      { id: 1, text: 'Watch a puppet show', done: false },
      { id: 2, text: 'Lennon Wall pic', done: false },
    ];

    위의 코드를 분석해보자.

    const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
    • tasks는 현재 상태
    • action은 상태를 업데이트하기 위한 객체 (어떤 동작을 할지 담긴 객체)
    • dispatch()는 상태를 바꾸는 요청을 reducer에 전달하는 함수
    • tasksReducer는 상태를 어떻게 바꿀지 정의한 로직
    • initialTasks는 초기 상태 값

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

    보통의 경우 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 정보를 전달한다. 그러나 중간에 많은 컴포넌트를 거쳐야 하거나, 애플리케이션의 많은 컴포넌트에서 동일한 정보가 필요한 경우에는 props를 전달하는 것이 번거롭고 불편할 수 있다. Context는 부모 컴포넌트가 트리 아래에 있는 모든 컴포넌트에 깊이에 상관없이 정보를 명시적으로 props를 통해 전달하지 않고도 사용할 수 있게 해준다.

    Props 전달하기의 문제점

    Props 전달하기는 UI 트리를 통해 해당 데이터를 사용하는 컴포넌트에 명시적으로 데이터를 전달하는 훌륭한 방법이다.
    하지만 이 방식은 어떤 prop을 트리를 통해 깊이 전해줘야 하거나, 많은 컴포넌트에서 같은 prop이 필요한 경우에 장황하고 불편할 수 있다. 데이터가 필요한 여러 컴포넌트의 가장 가까운 공통 조상은 트리 상 높이 위치할 수 있고 그렇게 높게까지 state를 끌어올리는 것은 “Prop drilling”이라는 상황을 초래할 수 있다.

    Props Drilling

    데이터를 사용할 트리의 내부 컴포넌트에 props를 전달하는 대신 “순간이동”시킬 방법이 있다면 좋지 않을까? 그때 React의 context를 사용하면 된다.

    Context: Props 전달하기의 대안

    Context는 부모 컴포넌트가 그 아래의 트리 전체에 데이터를 전달할 수 있도록 해준다.
    이렇게 하면 props를 통해 전달할 필요 없이 트리의 깊은 곳에 있는 컴포넌트에서 데이터를 사용할 수 있다. Context는 React의 기본 기능이며, React 애플리케이션에서 전역적으로 사용되는 데이터(예: 현재 사용자, UI 테마 등)를 제공하는 데 유용하다.

    1단계: Context 생성하기

    먼저 context를 만들어야 한다. 컴포넌트에서 사용할 수 있도록 파일에서 내보내야 한다.

    import { createContext } from 'react';
     
    export const LevelContext = createContext(1);

    createContext의 유일한 인자는 기본값이다. 여기서 1은 가장 큰 제목 레벨을 나타내지만 모든 종류의 값을(객체까지) 전달할 수 있다. 다음 단계에서 기본값이 얼마나 중요한지 알게 된다.

    2단계: Context 사용하기

    React에서 useContext Hook과 생성한 Context를 가져온다.

    // Heading.js
    import { useContext } from 'react';
    import { LevelContext } from './LevelContext';
     
    export default function Heading({ children }) {
      const level = useContext(LevelContext);
      const Tag = `h${level}`;
     
      return <Tag>{children}</Tag>;
    }

    useContext는 Hook이다. useState, useReducer와 같이 Hook은 React 컴포넌트의 바로 안에서만 호출할 수 있다. (반복문이나 조건문 내부가 아니다.) useContext는 React에게 Heading 컴포넌트가 LevelContext를 읽으려 한다고 알려준다.

    3단계: Context 제공하기

    <Provider>를 이용해 Context의 값을 하위 컴포넌트에게 전달할 수 있다.

    // Section.js
    import { LevelContext } from './LevelContext';
     
    export default function Section({ children }) {
      return <LevelContext.Provider value={2}>{children}</LevelContext.Provider>;
    }

    이제 이 Section 안에 있는 Heading 컴포넌트는 level === 2를 받게 된다.

    기능설명
    createContextContext를 생성 (기본값 포함 가능)
    useContext하위 컴포넌트에서 Context 값을 읽을 때 사용
    Provider하위 트리 컴포넌트에 Context 값을 공급

    Context를 사용하기 전에 고려할 것

    Context는 사용하기에 꽤 유혹적이다. 그러나 이는 또한 남용하기 쉽다는 의미이기도 한다. 어떤 props를 여러 레벨 깊이로 전달해야 한다고 해서 해당 정보를 context에 넣어야 하는 것은 아니다.

    다음은 context를 사용하기 전 고려해볼 몇 가지 대안들이다.

      1. Props 전달하기로 시작하기. 사소한 컴포넌트들이 아니라면 여러 개의 props가 여러 컴포넌트를 거쳐 가는 것은 그리 이상한 일이 아니다. 힘든 일처럼 느껴질 수 있지만 어떤 컴포넌트가 어떤 데이터를 사용하는지 매우 명확히 해준다. 데이터의 흐름이 props를 통해 분명해져 코드를 유지보수 하기에도 좋다.
      1. 컴포넌트를 추출하고 JSX를 children으로 전달하기. 데이터를 사용하지 않는 많은 중간 컴포넌트 층을 통해 어떤 데이터를 전달하는 (더 아래로 보내기만 하는) 경우에는 컴포넌트를 추출하는 것을 잊은 경우가 많다. 예를 들어 posts처럼 직접 사용하지 않는 props를 <Layout posts={posts} />와 같이 전달할 수 있다. 대신 Layout은 children을 prop으로 받고 <Layout><Posts posts={posts} /><Layout>을 렌더링하자. 이렇게 하면 데이터를 지정하는 컴포넌트와 데이터가 필요한 컴포넌트 사이의 층수가 줄어든다.

    만약 이 접근 방법들이 잘 맞지 않는다면 context를 고려해보자.

    • 테마 지정하기: 사용자가 모양을 변경할 수 있는 애플리케이션의 경우에 (e.g. 다크 모드) context provider를 앱 최상단에 두고 시각적으로 조정이 필요한 컴포넌트에서 context를 사용할 수 있다.
    • 현재 계정: 로그인한 사용자를 알아야 하는 컴포넌트가 많을 수 있다. Context에 놓으면 트리 어디에서나 편하게 알아낼 수 있다. 일부 애플리케이션에서는 동시에 여러 계정을 운영할 수도 있다. (e.g. 다른 사용자로 댓글을 남기는 경우.) 이런 경우에는 UI의 일부를 서로 다른 현재 계정 값을 가진 provider로 감싸 주는 것이 편리하다.
    • 라우팅: 대부분의 라우팅 솔루션은 현재 경로를 유지하기 위해 내부적으로 context를 사용한다. 이것이 모든 링크의 활성화 여부를 “알 수 있는” 방법입니다. 라우터를 만든다면 같은 방식으로 하고 싶을 것입이다.
    • 상태 관리: 애플리케이션이 커지면 결국 앱 상단에 수많은 state가 생기게 된다. 아래 멀리 떨어진 많은 컴포넌트가 그 값을 변경하고 싶어할 수 있다. 흔히 reducer를 context와 함께 사용하는 것은 복잡한 state를 관리하고 번거로운 작업 없이 멀리 있는 컴포넌트까지 값을 전달하는 방법이다.

    Context는 정적인 값으로 제한되지 않는다. 다음 렌더링 시 다른 값을 준다면 React는 아래의 모든 컴포넌트에서 그 값을 갱신한다. 이것은 context와 state가 자주 조합되는 이유다.
    일반적으로 트리의 다른 부분에서 멀리 떨어져 있는 컴포넌트들이 같은 정보가 필요하다는 것은 context를 사용하기 좋다는 징조이다.

    Posted inreact
    Written byEunwoo