리액트/개념뽀개기

다크모드에 프로바이더를 왜 쓰는가?

lamarcK 2025. 4. 10. 03:58

핵심은 테마 상태와 로직을 관리하는 부분사용자 인터랙션(버튼 클릭)을 통해 상태 변경을 시작하는 부분이 서로 다른 컴포넌트에 위치하는 경우가 일반적이기 때문에 Provider(Context API)가 유용하다는 것이다.

조금 더 풀어서 설명하면:

  1. 상태와 로직의 위치:
    • 테마 상태(isDarkMode)
    • 상태 변경 함수(toggleTheme)
    • localStorage 동기화 로직
    • document.body 클래스 변경 로직 (useEffect)
    • 이 모든 것들은 ThemeProvider 컴포넌트 안에 중앙 집중화되어 있다. 이 컴포넌트는 앱의 최상단 근처에 위치하여 하위 모든 컴포넌트에 영향을 줄 수 있도록 한다.
  2. 사용자 인터랙션 위치:
    • 사용자가 실제로 테마를 변경하기 위해 누르는 버튼이나 스위치는 보통 UI의 특정 위치(예: 헤더, 설정 메뉴 등)에 있는 별도의 컴포넌트(ThemeToggler 등)에 존재한다. 이 컴포넌트는 앱 구조상 ThemeProvider로부터 여러 단계 아래에 위치할 수 있다.
  3. 분리된 컴포넌트 간의 통신 필요성:
    • ThemeToggler 컴포넌트는 사용자가 버튼을 클릭했을 때, ThemeProvider 내부에 있는 toggleTheme 함수를 호출해야 한다.
    • 또한, ThemeToggler는 현재 테마 상태(isDarkMode)를 알아야 버튼의 텍스트나 아이콘(예: "다크 모드로 전환" / "라이트 모드로 전환")을 올바르게 표시할 수 있다.
  4. Context API의 역할 (다리 놓기):
    • Provider(Context API)는 이렇게 물리적으로 떨어져 있는 컴포넌트들 사이를 효과적으로 연결하는 다리 역할을 한다.
    • ThemeProvider가 value로 isDarkMode와 toggleTheme을 제공하면, ThemeToggler는 앱 구조상의 위치에 상관없이 useTheme() 훅을 사용하여 이 값과 함수에 직접 접근할 수 있다.

만약 Context API가 없다면?

앞서 언급했듯이, App.js에서 상태를 관리하고 ThemeToggler까지 props를 계속 내려보내야 하는 Prop Drilling이 발생한다. 이는 컴포넌트들이 불필요하게 서로의 존재와 props 전달 방식에 대해 알아야 하므로 **결합도(coupling)**가 높아지고 코드 관리가 어려워진다.

결론:

"UI를 변경하는 로직/상태를 가진 부분(ThemeProvider)"과 "그 변경을 촉발하는 버튼이 있는 부분(ThemeToggler)"이 서로 다른 컴포넌트이기 때문에, 이 둘 사이의 상태 공유와 함수 호출을 깔끔하게 처리하기 위해 Provider(Context API)를 사용하는 것이 매우 효과적이고 권장되는 방식이다.

 

사용자의 질문은 굉장히 중요한 지점을 파고든다. 다시 한번 정리해보자.

1. "상태 변수 하나 적용을 위해서 쓰는 것인가?"

  • 단순화된 예제에서는 isDarkMode라는 boolean 상태 하나를 관리하는 것처럼 보인다. 하지만 실제로는 이 상태와 밀접하게 관련된 여러 요소를 함께 다루고 있다.
    • 상태 값 (isDarkMode)
    • 상태 변경 로직 (toggleTheme 함수)
    • 상태를 영속시키기 위한 로직 (localStorage 접근)
    • 상태에 따른 부수 효과 로직 (useEffect 내의 document.body 클래스 조작)
    • 이 상태와 로직을 다른 컴포넌트에 공유하는 메커니즘
  • Context API는 이렇게 서로 관련된 데이터와 로직을 하나의 단위로 묶어 관리하고, 이를 필요한 곳에 효율적으로 전달하는 구조를 제공한다. 단순히 변수 하나만을 위한 것이라기보다는, 테마 관리라는 '기능' 또는 '관심사' 전체를 캡슐화하는 데 의미가 있다. 또한, 나중에 테마 관련 기능이 확장(예: 시스템 설정 따르기 옵션 추가)될 경우에도 Context 구조가 유연하게 대처할 수 있다.

2. "굳이 프로바이더 안 쓰고 버튼 컴포넌트랑 UI 변경 컴포넌트만 이어도 되는거 아닌가? 그것은 또 불가능한가?"

이 질문의 핵심은 "왜 중간 단계(Context)를 거쳐야 하는가? 버튼이 직접 UI 변경 로직을 실행하게 할 수는 없는가?" 이다.

결론부터 말하면, React의 일반적인 데이터 흐름과 설계 원칙 하에서는 '깔끔한 직접 연결'이 어렵거나 권장되지 않는다. 그 이유는 다음과 같다.

  • "UI 변경 컴포넌트"의 실체: 우리의 예제에서 실제 UI 변경은 document.body의 클래스를 바꾸는 useEffect 로직과, 그 변경에 반응하는 CSS가 담당한다. 이 useEffect 로직은 isDarkMode 상태 값에 의존한다.
  • 상태의 위치 문제:
    • 상태가 버튼 컴포넌트(ThemeToggler) 내부에 있다면? 버튼이 화면에 없을 때(예: 다른 페이지)는 상태가 존재하지 않거나 접근하기 어렵다. 또한, body 클래스를 변경하는 로직은 버튼의 존재 여부와 관계없이 앱 전역에 영향을 미쳐야 하는데, 버튼 내부 상태에 의존하는 것은 구조적으로 불안정하다.
    • 상태가 useEffect 로직과 함께 있다면? 그렇다면 ThemeToggler 버튼이 어떻게 이 로직/상태에 접근해서 toggleTheme을 실행시킬 수 있을까? 결국 둘 사이의 통신 방법이 필요하다.
  • React의 데이터 흐름: React는 기본적으로 **단방향 데이터 흐름(부모 -> 자식)**을 따른다. 부모는 자식에게 props를 통해 데이터를 전달하고, 자식은 부모로부터 받은 콜백 함수(props)를 실행하여 부모에게 이벤트를 알릴 수 있다. 임의의 두 컴포넌트(특히 계층 구조상 멀리 떨어진)가 서로 직접 통신하는 것은 일반적이지 않으며, 이를 위해서는 상태 끌어올리기(Lifting State Up) 또는 Context 같은 패턴이 필요하다.
  • 상태 끌어올리기(Lifting State Up): 여러 컴포넌트가 동일한 상태에 접근해야 할 때, 가장 가까운 공통 조상 컴포넌트로 상태를 이동시키는 것이 React의 기본 패턴이다. 우리의 경우, isDarkMode 상태와 관련 로직을 ThemeToggler와 body 클래스 조작 로직 모두에 접근 가능한 공통 조상(아마도 App.js)으로 끌어올릴 수 있다.
  • 상태 끌어올리기의 결과 -> Prop Drilling: 상태를 공통 조상으로 끌어올리면, 결국 그 상태와 변경 함수를 다시 필요한 자식 컴포넌트(ThemeToggler 등)까지 props로 내려보내야 한다. 이것이 바로 Context API가 해결하려는 Prop Drilling 문제이다.

결론:

'버튼 컴포넌트'와 'UI 변경 로직'을 직접 "잇는" 깔끔한 방법은 React의 표준적인 구조 내에서는 마땅치 않다. 상태를 공유해야 하는 가장 일반적인 방법은 상태를 공통 조상으로 끌어올리는 것인데, 이는 필연적으로 Prop Drilling을 유발할 수 있다.

Context API는 바로 이 '상태 끌어올리기' 이후 발생할 수 있는 Prop Drilling 문제를 해결하고, 관련 상태와 로직을 중앙에서 관리하며, 필요한 컴포넌트가 어디에 있든 쉽게 접근할 수 있도록 해주는, React에서 권장하는 '잇는' 방법이다. 따라서 단순히 상태 변수 하나만을 위해서라기보다는, React 애플리케이션의 구조적인 문제를 해결하고 코드를 깨끗하고 유지보수하기 좋게 만들기 위해 사용하는 것이다.