team logo icon

Props Drilling 해결하기(feat. State)

YB 김채현의 "React에 대해" 공유 과제 입니다. 1. Props Drilling 2. Solution01. 리액트 상태관리 3. Solution02. Children의 적극 사용 4. 불필요한 렌더링 줄이자!

아무 고민없이 구현에만 집중을 하며 코드를 짜본 적이 있으신가요?

저는 개발자보다 스파게티 요리사에 더 가까웠습니다.

컴포넌트를 계속해서 중첩시키고 필요한 데이터를 계속 넘기고 넘기고… 또 넘기고…

그런데 이러한 과정을 말하는 용어가 따로 있다고 합니다.

Props Drilling

Props Drilling은 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달할 때 중간 컴포넌트들을 거쳐야 하는 과정을 말합니다.

상위 컴포넌트에서 계속해서 하위 컴포넌트로 넘어가는 과정이 컴포넌트가 드릴처럼 밑으로 뚫고 내려간다라는 의미로 Props Drilling이라고 합니다.

import React from 'react';

function App() {
  const user = { name: '또이', age: 23 };

  return (
    <ComponentA user={user} />
  );
}

function ComponentA({ user }) {
  return (
    <ComponentB user={user} />
  );
}

function ComponentB({ user }) {
  return (
    <ComponentC user={user} />
  );
}

function ComponentC({ user }) {
  return (
    <div>
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
    </div>
  );
}

export default App;



위 코드에서는 ComponentC 컴포넌트에서 user 정보를 사용하기 위해 전달하는 과정으로 App > ComponentA > ComponentB > ComponentC 를 거쳐야 합니다.

ComponentAComponentBuser 데이터를 직접적으로 사용하지 않습니다.

그럼에도 불구하고 user 객체를 받아서 다시 하위 컴포넌트로 전달하는 역할만 합니다.


이러한 방식은 다음과 같은 문제를 일으킬 수 있습니다:

  1. 유지보수의 어려움 : 각 컴포넌트가 불필요하게 많은 props를 관리해야 하므로, 컴포넌트의 유지보수가 복잡해집니다.

  2. 코드의 가독성 저하 : 중간 컴포넌트들이 실제로 사용하지 않는 props를 전달하므로, 코드의 가독성이 떨어집니다.

  3. 재사용성 감소 : 중간 컴포넌트들이 특정 데이터에 종속되어 재사용하기 어려워집니다.


props 전달이 3~5개 정도라면 심각하게 문제가 되지 않지만,

여러번 중첩이 되면 코드의 복잡성을 증가하게 되고, 해당 props를 추적하기 힘들다는 문제가 발생합니다.

이러한 문제를 해결하기 위해서 탄생한 것이 바로 상태 관리입니다.

Solution 01. 리액트 상태 관리

리액트에서 상태 관리가 중요하다는 말은 흔히 들어볼 수 있는데요,

그런데 리액트에서 상태가 정확히 무엇일까요??


상태(State)는 컴포넌트의 데이터를 의미합니다.

이 데이터는 컴포넌트의 렌더링 결과에 영향을 미치고,

리액트는 상태를 기반으로 UI를 업데이트하기 때문에 리액트에서 상태 관리는 동적 데이터를 처리에 있어서 정말 중요합니다.

다양한 부분이 서로 일관되게 데이터를 공유하고,

사용자의 상호작용에 반응하기 위해 상태 관리는 필요하며

좋은 상태 관리는 성능을 최적화하고, 코드의 유지보수를 용이하게 하며, 개발 과정을 더욱 효율적으로 만듭니다.

리액트에서의 상태 관리 방법

리액트에서 상태 관리를 위한 기본적인 방법은 컴포넌트의 stateprops를 사용하는 것입니다.

state는 컴포넌트 내부에서 관리되는 데이터를 저장하며,

props는 부모 컴포넌트로부터 전달받은 데이터를 의미합니다.

컴포넌트의 state는 useState, setState 를 통해 업데이트를 하면, 컴포넌트가 재렌더링이 되며 사용자는 변경된 데이터를 보게 됩니다.

이런 방법은 간단한 상태 관리에 적합하지만, 개발의 규모가 커지면 여러 컴포넌트 간에 상태를 공유하고 관리해야 하는 경우가 많아지는 등 상태 관리의 복잡성이 증가하면 리액트의 기본적인 상태 관리 방법만으로는 힘들 수 있습니다.

상태를 효과적으로 관리하기 위해서는 다양한 레벨에서 상태를 쉽게 접근하고 업데이트할 수 있어야 하기 때문입니다.

따라서 일반적으로 상태 공유와 상태 변화에 따른 반응성을 효율적으로 관리하기 위해 redux와 같은 다양한 상태 관리 라이브러리나 패턴이 사용되고 있습니다.

Context API의 사용

useContext는 React의 Context API를 사용하여 컴포넌트 트리 전체에 걸쳐 데이터를 공유할 수 있게 해 주는 훅입니다.

이를 이용하면 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달할 필요 없이, 어떤 컴포넌트에서든 값을 공유할 수 있습니다.

사용 방법은 아래와 같습니다.

  1. createContext() 함수를 호출하여 Context 객체를 생성합니다. 이 Context 객체는 Provider와 Consumer로 구성됩니다.

  2. Provider는 Context로 전달할 값을 설정하는 컴포넌트입니다. 이 값은 Provider 컴포넌트의 props로 전달됩니다. 이렇게 설정된 값을 Consumer에서 사용할 수 있습니다.

  3. Consumer는 Context 값을 사용하는 컴포넌트입니다. 이 컴포넌트에서는 Context 객체를 가져와서 값을 사용합니다.

  4. useContext는 React 16.8 이상 버전에서 Context 값을 사용하는 훅입니다. Consumer과 동일하게, 해당 훅을 사용해 Context 객체를 가져와서 값을 사용합니다.

import React, { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button style={{ background: theme === 'dark' ? 'black' : 'white', color: theme === 'dark' ? 'white' : 'black' }}>Click me</button>;
}

export default App;



이 예시 코드에서 ThemeContext는 전체 테마 상태를 관리합니다.

ThemeContext.*Provider를 사용하여 theme 값을 "dark"로 설정하고, Toolbar 컴포넌트를 통해 ThemedButton까지 이 값을 전달합니다.

ThemedButton에서는 useContext를 사용하여 현재 테마 값을 직접 받아와 버튼의 스타일을 설정합니다.

중간 컴포넌트에서 props를 전달하지 않아도 된다는 장점이 있지만, 너무 남발하면 컴포넌트 간의 의존성이 높아지고 디버깅이 어려워질 수 있기 때문에 필요한 경우에만 사용하는 것이 좋습니다.

Solution 02. Children의 적극 사용

상위 컴포넌트는 parent component, 하위 컴포넌트는 child component로 부모-자식 관계가 설정되면 부모 컴포넌트 내부에서는 children prop을 통해 자식 컴포넌트 정보에 접근할 수 있습니다.

children prop은 컴포넌트에게 자식 컴포넌트를 포함시키는 방법으로 태그와 태그 사이의 모든 내용을 표시하기 위해 사용되는 특수한 props를 말합니다.

children prop을 사용하면, 특정 컴포넌트가 어떤 자식 컴포넌트들을 갖게 될지 미리 정의하지 않고도, 재사용 가능한 컴포넌트를 만들 수 있습니다.

상태를 직접 관리하지 않고, 컴포넌트 간의 데이터를 전달하거나 재사용 가능한 컴포넌트 패턴을 구성할 때 유용합니다.

const App = () => (
  <Category>
	  // li 태그는 여러개일 수 있음
    <li>First item.</li>
    <li>Second item.</li>
    <li>Another item.</li>
  </Category>
);

cosnt Category = (props) => {
	return <ul>{props.children}</ul>
}


children는 상태 관리와 함께 사용될 수 있으며, 특히 Context API와 함께 사용될 때 컴포넌트 간의 상태 전달을 용이하게 해 줄 수 있습니다.

Context API와 Children의 결합 사용 예

Context API와 children을 함께 사용하면, Context를 통해 제공된 데이터를 필요로 하는 컴포넌트들을 children을 통해 유연하게 구성할 수 있습니다.

이렇게 하면, Context에서 제공하는 데이터를 활용하는 여러 컴포넌트를 재사용하고, 다양한 위치에서 사용할 수 있게 됩니다.

import React, { createContext, useContext } from 'react';

// Context 생성
const UserContext = createContext();

// Context를 제공하는 부모 컴포넌트
function UserProvider({ children }) {
  const user = { name: '또이', age: 23 };

  return (
    <UserContext.Provider value={user}>
      {children}
    </UserContext.Provider>
  );
}

// Context를 사용하는 자식 컴포넌트
function UserProfile() {
  const user = useContext(UserContext);

  return (
    <div>
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
    </div>
  );
}

// App 컴포넌트에서 UserProvider 내부에 UserProfile 컴포넌트를 포함
function App() {
  return (
    <UserProvider>
      <UserProfile />
    </UserProvider>
  );
}

export default App;



  • UserProvider 컴포넌트는 UserContext를 통해 user 데이터를 하위 컴포넌트에게 제공합니다.

  • UserProfile 컴포넌트는 useContext 훅을 사용하여 UserContext에서 user 데이터를 가져옵니다.

  • App 컴포넌트는 UserProvider 내부에 UserProfile을 자식 컴포넌트로 포함시키며, UserProviderchildren으로서 UserProfile이 활용됩니다.

Children의 렌더링 최적화

const ChildComponent = () =>{
	console.log("ChildComponent is rendering!");
	return <div>Hello World!</div>
}

const ParentComponent = ({children}) =>{
	console.log("ParentComponent is rendering!");
    const [toggle, setToggle] = useState(false);
	return <div>
 		{children}
        <button onClick={()=>{setToggle(!toggle)}}>
        	re-render
        </button>
    </div>
}

const Container =() =>{
	return <div>
    	<ParentComponent>
        	<ChildComponent/>
        </ParentComponent>
    </div>
}


children prop을 활용하면 간접적으로 ChildComponentreturn하여 ChildrenComponent가 렌더링되지 않고 최적화를 달성할 수 있습니다.


❓ 어떻게 ChilrenComponent는 렌더링되지 않을까요?

children이란 말 그대로 prop일 뿐이며, ParentComponent에 children 속성을 주는 것과 완전히 동일한 표현이기 때문입니다.

ChildComponent의 경우, 그 내부 로직이나 표시되는 데이터가 변하지 않기 때문에, React는 자동적으로 최적화를 수행할 수 있습니다.


<Child/>React.createElement(Child,null,null) 와 같은 표현입니다.

따라서 아래와 같이 코드를 수정한다고 하더라도

const Child () =>{
	console.log("ChildComponent is rendering!");
	return <div>Hello World!</div>
}

const Parent = ({children}) =>{
	console.log("ParentComponent is rendering!");
    const [toggle, setToggle] useState(false);
	return <div>
 		{children}
        <button onClick={()=>{setToggle(!toggle)}}>
        	re-render
        </button>
    </div>
}

const Container =() =>{
	return <div>
    	<Parent children={<Child/>}/>
    </div>
}


React.createElement(Child,null,null)이 실행되는 것은 Container가 렌더링되며 Parent에 props을 넘겨줄 때 뿐이므로,

Parent가 리렌더링 된다고 해도 이전 렌더링에서 전달 받은 children 값을 그대로 사용합니다.

즉, Parent의 children은 애초에 object 형태인 상수로 전달 받았기 때문에 렌더링 이전과 비교해서 값이 달라지지 않아 리렌더링이 되지 않습니다.

children prop은 React.memo를 사용하지 않고도 Child component의 불필요한 렌더링을 방지해주는 장점이 있지만, Parent component의 불필요한 렌더링을 유발할 수 있다는 단점이 있습니다.

+) 불필요한 렌더링을 줄이자!

컴포넌트의 불필요한 렌더링을 줄이는 것이 바로 리액트 최적화의 첫 단계입니다.

React.memo, useMemo, useCallback과 같은 API를 사용하하면 렌더링을 줄일 수 있습니다.

  • React.memo : 컴포넌트의 props가 변경되지 않으면 컴포넌트의 렌더링을 건너뛰어 성능을 최적화

  • useMemo : 계산 비용이 높은 함수의 결과를 메모이징하여, 동일한 매개변수로 함수가 호출될 때 다시 계산하지 않고 메모리에서 결과를 반환하는 데 사용

  • useCallback : 함수를 props로 하위 컴포넌트에 전달할 때 유용. 메모이제이션된 콜백 함수를 반환하며, 종속성 배열에 있는 값들이 변경될 때만 함수를 다시 생성

마무리

리액트에서의 상태 관리와 성능 최적화는 반응성, 유지 보수성, 사용자 경험을 직접적으로 영향을 미치기 때문에 필수 요소입니다.

상태 관리는 데이터 흐름을 효율적으로 관리하고,

렌더링 최적화는 성능을 개선하여 더 나은 서비스를 제공할 수 있습니다.

이번 아티클을 작성하면서 앞으로 리액트 개발을 할 때,

어떻게 더 효율적인 개발을 할 수 있을지 공부의 필요성을 느낄 수 있었습니다.


참고

https://velog.io/@2ast/React-children-prop%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0feat.-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%B5%9C%EC%A0%81%ED%99%94

https://velog.io/@ahsy92/%EA%B8%B0%EC%88%A0%EB%A9%B4%EC%A0%91-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC%EC%99%80-Props-Drilling

https://f-lab.kr/insight/react-state-management-and-optimization-strategies


최신 아티클
lighthouse에 대해
문성희
|
2024.05.13
lighthouse에 대해
lighthouse에 대해
prettier, eslint, styleLint에 대해
이진
|
2024.05.10
prettier, eslint, styleLint에 대해
4주차 공유과제
Article Thumbnail
박채연
|
2024.05.10
Prettier, ESLint, StyleLint
prettier, eslint, stylelint