본문 바로가기

프론트엔드/ReactJS

Redux vs. React Context (1) - Context와 Redux에 대한 이해

이 글은 다음 페이지의 일부를 번역했습니다.

https://blog.isquaredsoftware.com/2021/01/context-redux-differences/

 

Blogged Answers: Why React Context is Not a "State Management" Tool (and Why It Doesn't Replace Redux)

Definitive answers and clarification on the purpose and use cases for Context and Redux

blog.isquaredsoftware.com

 

Context와 Redux에 대한 이해

어떤 도구든 제대로 사용하려면, 다음을 이해하는 게 중요합니다:

  • 목적이 무엇인지
  • 어떤 문제를 해결하고자 하는 것인지
  • 언제, 왜 처음 만들었는지

또한 현재 자신의 어플리케이션에서 해결하려는 문제를 이해하고, 문제를 가장 잘 해결하는 도구 - 다른 사람이 사용하라고 해서가 아니라, 도구가 유명해서가 아니라, 그 도구가 당신의 상황에 가장 잘 작동하는 도구 -를 선택하는 것도 중요합니다.

 

"Context vs. Redux"에 대한 대부분의 혼란은, 도구들이 실제로 어떤 일을 하는지, 어떤 문제들을 해결할 수 있는지에 대한 이해 부족에서 비롯됩니다. 따라서 Context나 Redux를 사용하는지 알기 위해선 도구들이 어떤 일을 하는지, 어떤 문제를 해결할 수 있는지를 명확히 정의해야 합니다.

 


React Context란 무엇인가?

먼저, 리액트 공식 문서에서 Context에 대해 어떻게 설명하는지 보고 넘어가겠습니다.

context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있습니다.

일반적인 리액트 애플리케이션에서 데이터는 위에서 아래로 (즉, 부모로부터 자식에게) props를 통해 전달되지만, 애플리케이션 안의 여러 컴포넌트들에 전해줘야 하는 props의 경우 (예를 들면 선호 로케일, UI 테마) 이 과정이 번거로울 수 있습니다. context를 이용하면, 트리 단계마다 명시적으로 props를 넘겨주지 않아도 많은 컴포넌트가 이러한 값을 공유하도록 할 수 있습니다.

 

값을 "관리(managing)" 하는 것에 대해서는 어떠한 말도 하지 않는 것에 주목하세요. - 공식 문서에서는 오로지 값을 "전달(passing)"하고 "공유"하는 것에 대해서만 언급합니다.

 

현재의 리액트 Context API(React.createContext())는 React 16.3.에서 처음 릴리즈되었습니다. 이것은 초기 버전의 리액트에서부터 사용 가능했지만, 주요 설계 결함이 있었던 legacy context API를 대체했습니다. legacy context의 주요 문제는 컴포넌트가 shouldComponentUpdate를 통한 렌더링을 건너 뛰면, context를 통해 전달된 값에 대한 업데이트가 "차단(blocked)"될 수 있다는 것이었습니다. 많은 컴포넌트가 성능 최적화를 위해 sholdComponentUpdate에 의존했기 때문에, legacyContext는 일반 데이터를 전달하는 데 쓸모가 없었습니다. createContext()는 이러한 문제를 해결하기 위해 설계되었고, 중간에 있는 컴포넌트가 렌더링을 건너뛰더라도 값에 대한 모든 업데이트가 자식 컴포넌트에 표시되도록(any update to a value will be seen in child components) 합니다.


Context 사용하기

앱에서 리액트 Context를 사용하려면 몇 가지 단계가 필요합니다.

  • 첫째로, context 객체 인스턴스를 만들기 위해 const MyContext = React.createContext()를 호출합니다.
  • 부모 컴포넌트에서, <MyContext.Provider value={someValue}>를 렌더링합니다. 이것은 context에 데이터의 일부를 넣습니다. value는 어떤 값이든 상관 없습니다(문자열, 숫자, 객체, 배열, 클래스 인스턴스, 이벤트 에미터 등).
  • 그리고 나서, provider로 감싸진 어떤 객체에서든, const theContextValue = useContext(MyContext)를 호출하면 됩니다. 

부모 컴포넌트가 다시 렌더링되고 context provider에 대한 새 참조를 value로 전달할 때마다, 해당 컨텍스트에서 읽는 구성 요소는 강제로 다시 렌더링됩니다.

 

가장 일반적으로, contex의 value는 다음과 같이 리액트 컴포넌트의 상태에서 옵니다.

function ParentComponent() {
  const [counter, setCounter] = useState(0);

  // Create an object containing both the value and the setter
  const contextValue = {counter, setCounter};

  return (
    <MyContext.Provider value={contextValue}>
      <SomeChildComponent />
    </MyContext.Provider>
  )
}

 

그러면 자식 컴포넌트는 useContext를 호출하고 값을 읽을 수 있습니다.

function NestedChildComponent() {
  const { counter, setCounter } = useContext(MyContext);

  // do something with the counter value and setter
}

 


Context의 사용 목적과 Use Cases

위 내용을 기반으로, Context가 실제로는 아무 것도 "관리"하지 않는다는 것을 알 수 있습니다. 대신에 그것은 파이프나 웜홀과 같습니다. <MyContext.Provider>를 사용하여 파이프의 맨 꼭대기에 무언가를 놓으면, 다른 컴포넌트가 useContext(MyProvider)를 사용하여 요청하는 파이프의 끝자락에서 그 무언가가 나타납니다.

 

따라서, Context를 사용하는 주요 목적은 "prop-drilling"을 피하는 것입니다. 값을 필요한 컴포넌트 트리의 모든 계층을 통해 명시적으로 prop으로 전달하는 대신, <MyContext.Provider>로 감싸진 모든 컴포넌트는 필요에 따라 값을 가져오기 위해 useContext(MyContext)라고 말할 수 있습니다. 이렇게 하면, prop을 전달하기 위한 추가적인 로직을 모두 작성할 필요가 없기 때문에  코드가 단순해집니다.

 

개념적으로 이것은 "의존성 주입(Dependency Injection)"의 한 형태입니다. 자식 컴포넌트에는 특정 타입의 값이 필요하지만, 해당 값 자체를 생성하거나 설정하려고 하지는 않습니다. 대신 런타임 시, 일부 부모 컴포넌트가 해당 값을 전달한다고 가정합니다.


 Redux란 무엇인가?

비교를 위해, the "Redux Essentials" tutorial in the Redux docs의 설명을 봅시다.

Redux는 "액션"이라는 이벤트를 사용하여 애플리케이션 상태를 관리하고 업데이트하기 위한 패턴 및 라이브러리입니다. 이는 상태가 예측 가능한 방식으로만 업데이트될 수 있도록 보장하는 규칙과 함께 전체 애플리케이션에서 사용해야 하는 상태에 대한 중앙 저장소(centralized store) 역할을 합니다.

Redux는 "전역" 상태를 관리하는 데 도움이 됩니다. (전역 상태 - 애플리케이션의 많은 부분에 필요한 상태)

Redux에서 제공하는 패턴과 도구를 사용하면 애플리케이션의 상태가 언제, 어디서, 왜, 어떻게 업데이트되고 이러한 변경이 발생할 때 애플리케이션 로직이 어떻게 작동하는지 더 쉽게 이해할 수 있습니다.

 

이 설명에 주목하세요:

  • "상태 관리"를 특정해서 언급했습니다.
  • Redux의 목적은 시간 경과에 따른 상태변화에 대한 이해를 돕는 것이라고 말했습니다.

역사적으로 Redux는 원래 리액트가 나온 지 1년 후인 2014년, Facebook에서 처음 제안한 패턴인 "Flux Architecture"의 구현으로 만들어졌습니다. 그 발표 이후 커뮤니티는 Flux 개념에 대한 다양한 접근 방식으로 Flux에서 영감을 받은 수십 개의 라이브러리를 만들었습니다. Redux는 2015년에 나왔고, 최고의 디자인을 가지고 있고, 사람들이 해결하려고 하는 문제와 일치하고, 리액트와 훌륭하게 작동했기 때문에, "Flux Wars"에서 빠르게 승리했습니다.

 

구조적으로 Redux는 두 가지를 강조합니다.

- 예측 가능한 "reducer" 함수로 최대한 많은 코드를 작성하는 데 도움이 되는, 함수형 프로그래밍 원칙(functional programming principle)을 사용하는 것

- "어떤 이벤트가 발생했는지"에 대한 아이디어를 "해당 이벤트가 발생할 때 상태가 업데이트되는 방법"을 결정하는 논리와 분리하는 것

 

Redux는 또한 부작용 처리(handling side effect)를 포함하여 Redux 저장소의 기능을 확장하는 방법으로 미들웨어를 사용합니다.

 

Redux에는 또한 Redux Devtools가 있어 시간 경과에 따른 앱의 작업 및 상태 변경 기록을 볼 수 있습니다.


Redux와 React

Redux 자체는 UI에 구애받지 않습니다. 모든 UI 레이어(React, Vue, Angular, vanilla JS 등)와 함께 사용하거나, UI 없이 사용할 수 있습니다.

 

Redux는 리액트와 함께 가장 일반적으로 사용됩니다. React-Redux 라이브러리는 리액트 구성 요소가 Redux 상태에서 값을 읽고 작업을 전달하여 Redux 저장소와 상호 작용할 수 있도록 하는 공식 UI 바인딩 레이어입니다. 따라서 대부분의 사람들이 "Redux"를 언급할 때 실제로는 "Redux 저장소와 React-Redux 라이브러리를 함께 사용하는 것"을 의미합니다.

 

React-Redux를 사용하면 애플리케이션의 모든 리액트 구성 요소가 Redux 저장소와 통신할 수 있습니다. 이것은 React-Redux가 내부적으로 Context를 사용하기 때문에 가능합니다. 그러나 React-Redux는 현재 상태 값이 아니라 컨텍스트를 통해서만 Redux 저장소 인스턴스를 전달한다는 점에 유의하는 것이 중요합니다! 이것은 위에서 언급한 것처럼 실제로 의존성 주입을 위해 Context를 사용하는 예입니다. 우리는 Redux에 연결된 리액트 컴포넌트가 하나의 Redux 저장소와 통신해야 한다는 것을 알고 있지만, 컴포넌트를 정의할 때 어떤 Redux 저장소가 있는지 알지 못하거나 신경 쓰지 않습니다. 실제 Redux 저장소는 React-Redux <Provider> 컴포넌트를 사용하여 런타임 시 컴포넌트 트리에 주입됩니다.

 

이 때문에 특히 React-Redux 또한 prop-drilling을 피하기 위해 사용할 수도 있습니다. 이는 React-Redux가 내부적으로 Context를 사용하기 때문입니다. 새 값을 직접 <MyContext.Provider>에 명시적으로 입력하는 대신, 해당 데이터를 Redux 저장소에 넣은 다음 어디에서나 액세스할 수 있습니다.


(React-)Redux의 사용 목적과 Use Cases

Redux를 사용하는 주된 이유는 Redux 문서에 다음과 같이 나와있습니다.

Redux에서 제공하는 패턴과 도구를 사용하면 애플리케이션의 상태가 언제, 어디서, 왜, 어떻게 업데이트되고 이러한 변경이 발생할 때 애플리케이션 로직이 어떻게 작동하는지 더 쉽게 이해할 수 있습니다.

 

Redux를 사용하려는 추가적인 이유들이 있습니다. "prop-drilling을 피하는 것"은 다른 이유 중 하나입니다. React의 legacy context가 고장났었고 React-Redux가 올바르게 작동했기 때문에, 많은 사람들이 특히 prop-drilling을 피하기 위해 초기에 Redux를 선택했습니다.

 

Redux를 사용해야 하는 다른 유효한 이유는 다음과 같습니다:

  • UI 레이어와 완전히 분리된 상태 관리 로직을 작성하길 원할 때
  • 서로 다른 UI 레이어 간에 상태 관리 로직을 공유할 때(예: AngularJS에서 React로 마이그레이션 중인 애플리케이션)
  • 액션이 디스패치될 때 추가적인 로직을 더하기 위한 Redux 미들웨어의 기능을 사용하고 싶을 때
  • Redux 상태의 일부를 유지하고자 할 때
  • 개발자가 리플레이할 수 있는 버그리포트를 활성화하고자 할 때
  • 개발 중 로직 및 UI의 더 빠른 디버깅을 원할 때

Dan Abramov는 2016년에 You Might Not Need Redux라는 게시물을 작성할 때 이러한 사용 사례를 나열했습니다.