📕

1.1 Context API 개념

1.1.1 React 컴포넌트의 상태

1) 컴포넌트의 데이터 변경 및 전달

React는 컴포넌트 기반의 아키텍처를 사용합니다. UI를 작은 컴포넌트로 나누고 이 컴포넌트들을 조합하여 완전한 UI를 생성합니다. 컴포넌트 내부에서 데이터를 동적으로 변경해야 하는 경우 ‘상태(State)’를 사용하며, 이를 통해 컴포넌트는 자체적으로 데이터를 관리하고 업데이트할 수 있습니다. 컴포넌트 간 데이터 전달은 단방향으로 ‘props’를 통해 이루어지며, 상위 컴포넌트가 하위 컴포넌트에 데이터를 전달합니다. 하위 컴포넌트는 해당 데이터를 읽을 수만 있고 직접 수정할 수 없습니다. 이러한 React의 데이터 전달 방식은 데이터를 예측 가능하게 하며, 안정성을 높여줍니다.

2) props

React에서 props는 ‘속성(properties)’의 줄임말로, 상위 컴포넌트에서 하위 컴포넌트로 데이터를  전달하는 데 사용되는 방식입니다. 상위 컴포넌트는 하위 컴포넌트를 호출할 때, 해당 컴포넌트에 필요한 데이터를 props로 전달합니다. 이 데이터는 키-값 쌍으로 구성되며, 다양한 데이터 타입을 포함할 수 있습니다. 하위 컴포넌트는 props를 받아와 컴포넌트 내부에서 사용합니다. props는 함수 매개변수처럼 컴포넌트 함수의 인자로 전달되며, 컴포넌트 내부에서 props를 참조하여 데이터를 사용합니다.
 
props 예시 코드입니다.
// 상위 컴포넌트 function ParentComponent() { return ( <ChildComponent name="John" message="Hello from parent!" /> ); } // 하위 컴포넌트 function ChildComponent(props) { return ( <div> <h2>{props.name}</h2> {/* props.name === "John" */} <p>{props.message}</p> {/* props.message === "Hello from parent" */} </div> ); }
 

3) State

State는 리액트에서 사용하는 일반 자바스크립트 객체이며 렌더링 결과물에 영향을 주는 데이터를 가지고 있으며  함수 내에서 선언된 state는 각 컴포넌트 안에서 관리합니다.
 
state는 Onclick 같은 사용자 이벤트 또는 네트워크 상태에 따라 변경될 수 있습니다. 컴포넌트가 마운트 될 때, state의 기본값으로 시작하고 컴포넌트에서 자체적으로 state를 관리하지만 초기 상태를 설정하는 것 말고는 하위 컴포넌트의 state를 변경할 수 있는 권한이 없습니다.
 
즉, state는 하위 컴포넌트에서 직접 변경하는 것은 권장되지 않고 만약, state 값을 변경해야 한다면 해당 컴포넌트 내부에서 setState()를 사용해야 합니다. state를 자주 쓰면 관리에 더 용이할 것이라고 생각이 들 수 있겠지만, state를 많이 사용하게 되면 복잡성을 증가시키고 관리가 어려워지고 state 업데이트를 처리하는 데 많은 시간이 소요될 수 있기 때문에 상호작용해야 하는 상황이 아니라면 많은 state를 갖는 것을 피해야 합니다.
 

(1) setState

setState는 state를 업데이트하고 리렌더링하는 데 사용됩니다. setState를 사용해서 state를 업데이트하면 리액트는 변경된 state를 감지하고 렌더링 하여 화면을 다시 그리게 됩니다. 이러한 기능은 동적 데이터를 다루는 데 유용하게 쓰입니다.
setState 메서드는 2가지 형태로 사용됩니다.
  • 객체 형태
this.setState는 현재 컴포넌트의 상태를 업데이트하는 메서드이며 새 count를 이전 count 값에 10을 더한 값으로 업데이트하고 있습니다.
this.setState({count: this.state.count + 10});
  • 함수 형태
setState가 함수 형태로 사용될 때 리액트가 일관성 있게 이전 상태 값을 보장할 수 있도록 콜백 함수로 처리합니다. 객체 형태와 마찬가지로 이전 상태를 인자로 받아 새 상태를 반환합니다.
this.setState((prevState) => return { count: prevState.count + 1 }; });
이러한 방법은 비동기 업데이트나 이전 상태를 기반으로 새 상태를 계산해야 할 때 유용합니다. 즉, setState를 사용할 때 상태 업데이트는 항상 이전 상태를 기반으로 이루어져야 하며, 상태를 직접적으로 변경해서는 안 됩니다.
 

(2) class형 컴포넌트

함수형 컴포넌트 이전에 사용했던 컴포넌트 사용 방식입니다. class형 컴포넌트는 state와 Life Cycle 메서드를 직접 사용할 수 있습니다. 요즘은 거의 함수형 컴포넌트로 더 쉽고 편리하게 사용하지만, 프로젝트의 유지 보수를 위해서 알아두면 좋습니다.
import React, {Component} from 'react'; class Counter extends Component { constructor(props){ super(props); this.state = { count : 0, }; } increaseFunc = () => { this.setState({count: this.state.count + 10}); } render(){ return( <div> <p>{this.state.count}</p> <button onClick={this.increaseFunc}>더하기</button> </div> ) } }
 

(3) 함수형 컴포넌트 (useState)

class형 컴포넌트에서는 state 객체로 초깃값을 정하고, this.setState 함수로 state의 값을 변경하는 방식이었지만 함수형 컴포넌트에서는 useState 훅을 사용하여 상태를 관리합니다. 여기서 useState란 리액트의 기본 훅 중 하나로, 컴포넌트에서 state를 추가할 때 사용하며 컴포넌트 내부에서 상태 값을 생성하고 업데이트할 수 있습니다. 이 함수는 배열의 형태로 첫 번째 요소는 현재 상태 값, 두 번째 요소는 상태 값을 업데이트하는 함수이며 두 번째 요소에는 state 이름 앞에 set을 붙여서 사용하는 게 일반적입니다. state에 직접 할당하지 않고, setState 함수가 state를 변경하고 해당 컴포넌트와 자식 컴포넌트들까지 리렌더링 됩니다. 함수 컴포넌트의 state는 class와 달리 객체일 필요가 없고, 모든 타입을 사용할 수 있습니다.
import React, {useState} from 'react' function Counter() { const [count, setCount] = useState(0); return ( <div> <p>카운트 : {count}</p> <button onClick={() => {setCount(count + 1)}}></button> </div> ) } export default Counter;
위 코드에서 useState를 이용하여 count라는 상태 변수를 생성하고 초깃값을 useState 괄호 안에 0으로 설정했습니다. 버튼 클릭 시 setCount 함수를 사용하여 count 상태를 업데이트하고, 함수를 호출하여 상태를 변경할 수 있습니다.
 
그리고 State를 변경하는 또 한 가지 방법은 위에서 설명한 함수 형태로 넣는 방법입니다.
<div> <p>카운트 : {count}</p> <button onClick={() => {setCount((current) => { return current + 1 }) }}>클릭</button> </div>
함수를 넣는 경우 함수가 반환하는 값으로 State값이 변경됩니다. 그렇기에 함수형 컴포넌트는 현재 값을 기반으로 상태를 변경하고자 할 때 함수를 넣어 사용하는 방법을 권장합니다.
 
앞서 배웠던 props와 state 요약점을 표로 보여드리겠습니다.
언제 사용하는가?
데이터가 변하나?
데이터를 어디서 가지고있나?
State
데이터를 화면에 보여주거나 사용자 입력을 처리하는 데 사용
컴포넌트가 데이터를 관리하며 시간이 지남에 따라 변경 가능
State를 선언한 컴포넌트에서 자체적으로 관리
Props
데이터를 하위 컴포넌트에 전달하는 데 사용
한 번 설정되면 props는 변경 불가능
데이터가 외부 컴포넌트로 전달 가능
 

1.1.2 상태관리 라이브러리의 필요성

우리가 만든 리액트 서비스가 커지고 state가 많아지면서 기존의 리액트만으로는 서비스를 관리하고 유지하기 힘들어집니다. 이를 해결하고 조금 더 체계적으로 관리하기 위해 상태관리 라이브러리가 필요합니다.

1) 상태관리 라이브러리란?

상태관리 라이브러리란 리액트에서 상태, 즉 state를 효율적으로 관리하기 위한 도구를 말합니다. React는 사용자 인터페이스를 만들거나 SPA(single page application) 개발 등 다양한 상황에서 많이 사용되지만, 대규모 애플리케이션을 개발하거나 복잡한 상태를 관리해야 할 때, 상태 관리 라이브러리의 필요성이 높아집니다. 이러한 라이브러리를 사용함으로써 상태를 효과적으로 관리하고, 컴포넌트 간의 데이터 공유 및 통신을 단순화하며, 애플리케이션의 유지보수성을 향상 시킬 수 있습니다.
 

2) Props drilling

React에서 상태(state)가 변경되면 컴포넌트가 리렌더링(re-rendering) 되면서 페이지 변동이 일어나게 됩니다. 이때, state는 props를 통해 상위 컴포넌트에서 하위 컴포넌트로 직접 전달됩니다. React는 단방향(하향식) 데이터 바인딩을 지원하기 때문에 하위 컴포넌트에서 상위 컴포넌트로 state를 전달하는 것은 불가능합니다.
 
간단한 프로젝트에서는 props를 통한 단방향 데이터 흐름이 오히려 장점을 가질 수 있습니다. props를 이용하면 컴포넌트 간에 데이터를 가장 쉽고 빠르게 전달할 수 있습니다. 또한 작은 규모의 애플리케이션에서는 데이터의 추적이 쉬워지고, 컴포넌트 간의 의존성이 낮아져 코드 수정이나 유지 보수가 용이해집니다. 하지만, 프로젝트 규모가 점점 커지고, 컴포넌트와 관리해야 할 state의 개수가 많아지면 props를 통해 데이터를 전달하는 것이 어려워집니다.
 
props drilling이 무엇인지 알아보기 전에 위의 간단한 예시로 이해해 봅시다. 철수에게 책을 전달하라고 학생들에게 이야기했을 때, 책은 철수에게 한 번에 전달되지 않고, 중간의 학생들을 거쳐서 전달됩니다. 중간의 학생들은 책이 필요하지 않지만 철수에게 가려면 꼭 거쳐야 합니다. 전달하다가 중간에 책이 찢어질 수도 있고 많은 사람을 통해 전달해야 하기에 선생님의 말이 제대로 전달되지 않아 철수가 아닌 영희에게 전달될 수도 있다는 단점이 있습니다.
[그림] props drilling 예시1[그림] props drilling 예시1
[그림] props drilling 예시1
이러한 상황처럼 우리는 개발을 하다가 props를 통해 state를 하위 컴포넌트에 넘기고, 넘기고, 넘기고를 반복했던 경험을 가지고 있을 것입니다. 우리가 props를 상위 컴포넌트에서 하위 컴포넌트로 여러 번 반복해서 전달했던 것이 바로 props drilling 입니다. props drilling이란 props를 통해 데이터를 전달하는 과정에서 중간 컴포넌트는 데이터가 필요하지 않아도 하위 컴포넌트에 전달하기 위해 props를 받아 단계별로 전달해야 하는 과정을 말합니다.
[그림] props drilling 예시2[그림] props drilling 예시2
[그림] props drilling 예시2
 
위의 그림을 코드로 나타낸 예시입니다.
import React from 'react'; function ComponentA() { const data = "props drilling!"; return <ComponentC data={data} />; } function ComponentC({ data }) { return <ComponentE data={data} />; } function ComponentE({ data }) { return <ComponentG data={data} />; } function ComponentG({ data }) { return <ComponentH data={data} />; } function ComponentH({ data }) { return <>{data}</>; } function App() { return <ComponentA />; } export default App;
이 props drilling은 컴포넌트 계층 구조가 복잡해지고 깊어질수록 문제가 더 심각해집니다. 불필요한 props를 전달하다가 중간에 props 전달이 누락될 수도 있고, props의 이름이 중간에 임의로 변경될 경우 추적이 어려워지는 것과 같이 데이터 훼손의 문제점을 가지고 있습니다.
 
또한 중첩된 컴포넌트를 통해 props가 전달되면 가독성이 떨어지고, 데이터의 흐름이 복잡해지므로 코드를 유지보수하는데 어려움이 생길 수 있습니다. 이제 우리는 대규모 애플리케이션에서 props를 통한 전달 방법이 꽤 비효율적이라는 것을 알았습니다.
 
이러한 문제를 해결하기 위해 리액트 내장 hook인 Context API나 redux, recoil과 같은 상태 관리 라이브러리를 사용합니다. 이를 통해 데이터를 직접 props로 전달하지 않고도 컴포넌트 간에 쉽게 공유가 가능합니다.
 

1.1.3 Context API

1) Context API란?

Context API는 리액트 프로젝트에서 전역적으로 사용할 데이터가 있을 때 유용한 기능입니다. 사용자 로그인 정보, 애플리케이션 환경 설정, 테마 등 프로젝트 전반에서 필요한 데이터들을 관리할 때 주로 사용합니다.
작은 프로젝트 수준을 넘어서 수많은 데이터를 관리해야 하는 대규모 프로젝트를 만들거나 반복적으로 만들고 props 전달자 역할을 하던 컴포넌트를 재사용 가능한 컴포넌트로 만들고 싶다면 리액트에 내장된 상태 관리 라이브러리인  Context API를 사용하면 좋습니다.
 

2) Context API를 사용한 전역 상태 관리 흐름

정리하자면, 리액트 애플리케이션은 컴포넌트 간에 데이터를 props로 전달하기 때문에 여러 컴포넌트가 필요로 하는 데이터는 주로 최상위 컴포넌트인 App의 state에 넣어서 관리합니다.
 
[그림] 일반적인 전역 상태 관리 흐름[그림] 일반적인 전역 상태 관리 흐름
[그림] 일반적인 전역 상태 관리 흐름
그림으로 살펴보겠습니다. 어떤 프로젝트의 컴포넌트 구조입니다. 여기서 J 컴포넌트는 전역 상태를 업데이트하고, D와 G 컴포넌트는 업데이트된 상태를 렌더링 하는 작업을 진행한다고 가정해 봅니다. 그렇다면 App 컴포넌트에서 상태와 업데이트 함수를 정의하고, 이 값을 D 컴포넌트와 G 컴포넌트에 전달하기 위해 많은 컴포넌트를 거쳐야 합니다. D의 경우 App -> A -> B -> D의 흐름으로 데이터가 전달되고 G의 경우 App -> E -> F -> G의 흐름으로 전달될 것입니다. J 컴포넌트에 상태 업데이트 함수를 전달할 때도 App -> E -> F -> H -> J와 같이 복잡하게 거쳐서 전달해야 합니다. 앞서 살펴본 props drilling issue가 발생하게 되는 것입니다.
 
 
[그림] Context API를 사용한 전역 상태 관리 흐름[그림] Context API를 사용한 전역 상태 관리 흐름
[그림] Context API를 사용한 전역 상태 관리 흐름
하지만, Context API를 사용하면 상태 값과 업데이트 함수를 props로 하나하나 전달할 필요 없이, 만들어 둔 Context로부터 단 한 번에 원하는 값을 받아와 사용할 수 있습니다.