일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 백준
- 리액트
- 제로초
- js
- 타입스크립트
- til
- SQL 고득점 Kit
- 프로그래머스
- codestates
- 리트코드
- 정재남
- 회고
- 알고리즘
- 코드스테이츠
- 렛츠기릿 자바스크립트
- javascript
- python
- 파이썬
- 4주 프로젝트
- 코어 자바스크립트
- 자바스크립트
- programmers
- 손에 익히며 배우는 네트워크 첫걸음
- 2주 프로젝트
- 리덕스
- 타임어택
- 타입스크립트 올인원
- 토익
- LeetCode
- HTTP
- Today
- Total
Jerry
Props Drilling과 Context API 본문
리액트에 대해서 공부하는 사람이라면 대부분 듣거나 알고 있을 개념인 Props Drilling에 대해서 공부하는 도중..
Props Drilling에 대한 해결책으로 여러 라이브러리나 리액트 내부 기능이 존재했다.
리액트에서 제공하는 상태 관리 기능은 Context API이 존재하고,
상태 관리 라이브러리에는 Redux, Zustand, Recoil, Jotai 같은 것들이 대표적으로 있다.
거의 모든 상태관리 라이브러리들은 이 api를 기반으로 개발되었다고 한다.
먼저, Props Drilling에 대해 알아보았다.
기존에 상태 관리하는 기능이나 라이브러리가 없었던 시절(?), 컴포넌트 간에 상태를 공유하기 위해 Prop을 여러 계층을 통해 전달하는 것이 일반적이었다. 즉, Props Drilling은 상위 컴포넌트에서 하위 컴포넌트로 props를 계속 전달하는 방식을 뜻한다. 이는 번거로움과 필요하지 않은 컴포넌트까지 프롭을 전달해야 했기 때문에 컴포넌트 구조 복잡도가 상승했다.
예를 들면, App > Viewer > ViewerContent > ViewerTest1 > ViewerTest2의 구조가 있다고 가정해보자.
App에서 name이란 값을 ViewerTest2 컴포넌트에서 사용하고자 한다면, App 컴포넌트부터 ViewerTest2 컴포넌트까지 매번 컴포넌트를 경유해야 한다. 그 사이에 name값이 필요없는 중간 컴포넌트도 값전달을 위해 데이터를 받아서 전달 해야했고 이런 문제가 계층이 깊어질수록 유지보수가 어려워려졌다.
이에 따라, 코드 가독성이 떨어지고 구성 요소가 부모 구성 요소와 밀접하게 결합되어 재사용성 하락, 규모가 커질 수록 확정성 문제로 인해 복잡성 심하, 각 레벨에서 수동으로 props 전달로 불필요한 코드 중복 발생 등 많은 문제가 존재하게 된다.
import React from "react";
// Parent component that holds the message and passes it down
function App () {
const message = "Hello from Parent";
return (
<div>
<Viewer message={message} />
</div>
);
}
function Viewer({ message }) {
return (
<div>
<ViewerContent message={message} />
</div>
);
}
function ViewerContent({ message }) {
return (
<div>
<ViewerTest1 message={message} />
</div>
);
}
function ViewerTest1({ message }) {
return (
<div>
<ViewerTest2 message={message} />
</div>
);
}
function ViewerTest2({ message }) {
return (
<div>
<p>Message: {message}</p>
</div>
);
}
export default function App() {
return (
<div>
<App />
</div>
);
}
Context API
이렇듯, 상태 관리 방식은 성능과 개발 과정에 중요 영향을 미친다.
위와 같은 Props Drilling을 해결하기 위해 React Context API와 상태 관리 라이브러리가 도입되었다.
먼저, Context API는 명시적으로 prop를 전달하지 않고도 컴포넌트 트리 전체에 걸쳐 데이터를 공유할 수 있는 방법을 제공한다.
이는 위 문제를 해결하고 전역 상태 관리에도 유용하다. 아무리 깊은 컴포넌트 트리에서도 쉽게 상태를 공유할 수 있기 때문이다. Context는 이렇듯 사용하기 쉽고, 리액트 자체 제공 기능이라는 장점이 존재한다.
import React, { useContext } from 'react';
// 기본 값을 가진 컨텍스트 생성
// 새로운 컨텍스트를 초기화
const ThemeContext = React.createContext('light');
function App() {
return (
// Provider를 사용하여 컨텍스트에 값(value) 전달
// 컨텍스트 변경 사항을 구독할 수 있는 구성 요소
// Provider 아래의 모든 컴포넌트는 Provider에 전달된 값을 액세스할 수 있음
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>
);
}
function ThemedButton() {
// Consumer를 사용하여 현재 테마 값을 읽음
// 컨텍스트 변경 사항을 구독하는 React 컴포넌트
// 텍스트를 구독할 수 있음
return (
<ThemeContext.Consumer>
{theme => <button>{theme === 'dark' ? '다크 모드' : '라이트 모드'}</button>}
</ThemeContext.Consumer>
);
}
// React에서 hooks가 도입되면 함수형 컴포넌트에서 컨텍스트에 직접 액세스 쉬워짐
// useContext 훅은 Context.Consumer 컴포넌트로 컴포넌트를 감싸지 않고도 컨텍스트의 값을 직접 액세스 가능
function ThemedButton() {
// useContext 훅을 사용하여 현재 테마 값을 읽음
const theme = useContext(ThemeContext);
return (
<button>{theme === 'dark' ? '다크 모드' : '라이트 모드'}</button>
);
}
Context와 useReducer
더 복잡한 전역 상태 관리를 위해 Context API와 useReducer 훅을 결합하여 Redux와 유사한 전역 상태 저장소를 만들 수 있다.
import React, { useReducer, createContext } from 'react';
// 컨텍스트 정의
const StateContext = createContext();
// 리듀서 함수 정의
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
이렇듯,
전역 상태 관리가 간편
-> context를 통해 props drilling 문제 해결 및 상태 공유 가능
리액트의 친숙한 방식
-> 별도 외부 라이브러리 설정 필요 없음
중앙집중식 상태 관리
-> context로 중앙 상태 관리 ex) 테마 설정, 사용자 인증 정보, 알림 메시지
컴포넌트 간 데이터 전달 최적화
-> context로 Prop를 중간 컴포넌트 거치지 않고 직접 데이터 사용 가능
중간 컴포넌트의 불필요한 재렌더링 방지
-> 중간 컴포넌트를 거쳐 props 통해 데이터 전달 시 발생하는 불필요한 리렌더링 방지
단순한 프로젝트나 작은 규모에서 효과적
-> 설정 간단하여 코드 양도 적다 = 프로젝트가 작거나 상태 단순한 경우 적합
Context API를 사용하여 컴포넌트 간 데이터 전달을 간소화하고 props drilling 문제 해결함
중첩된 컴포넌트 구조에서 데이터 전달하는 대신, context를 활용하여 직접 데이터 가져올 수 있게 함
코드는 더 깔금해지며 관리 용이, 성능적인 측면에서도 효율적인 방법
리액트 내장 기능으로 빠른 구현 가능
Context API 단점
지금부턴 단점에 대해 알려보겠다.
리렌더링 문제
-> Context.Provider의 value 변경 시, 해당 컨텍스트를 구독하는 모든 컴포넌트가 리렌더링됨
-> state를 사용하지 않는 컴포넌트도 불필요한 리렌더링 발생 가능
import React, { createContext, useState, useContext } from "react";
const ModalContext = createContext(false);
const ModalProvider = ({ children }) => {
const [show, setShow] = useState(false);
return (
<ModalContext.Provider value={{ show, setShow }}>
{children}
</ModalContext.Provider>
);
};
const ModalToggleButton = () => {
const { setShow } = useContext(ModalContext);
console.log("ModalToggleButton 렌더링됨");
return <button onClick={() => setShow((prev) => !prev)}>모달 토글</button>;
};
const Modal = () => {
const { show } = useContext(ModalContext);
console.log("Modal 렌더링됨");
return show ? <div>나 모달</div> : null;
};
const App = () => (
<ModalProvider>
<ModalToggleButton />
<Modal />
</ModalProvider>
);
export default App;
- ModalToggleButton은 show 상태를 사용하지 않음에도 불구하고, ModalContext.Provider의 value={{ show, setShow }}가 변경되면 불필요하게 리렌더링됨.
- setShow만 필요한데, show도 같이 변경되면서 구독하지 않아도 영향을 받음.
- 해결방법 : Context 분리
// 해결방법 : Context 분리
const ModalStateContext = createContext(false);
const ModalDispatchContext = createContext(() => {});
const ModalProvider = ({ children }) => {
const [show, setShow] = useState(false);
return (
<ModalStateContext.Provider value={show}>
<ModalDispatchContext.Provider value={setShow}>
{children}
</ModalDispatchContext.Provider>
</ModalStateContext.Provider>
);
};
const ModalToggleButton = () => {
const setShow = useContext(ModalDispatchContext);
console.log("ModalToggleButton 렌더링됨");
return <button onClick={() => setShow((prev) => !prev)}>모달 토글</button>;
};
const Modal = () => {
const show = useContext(ModalStateContext);
console.log("Modal 렌더링됨");
return show ? <div>나 모달</div> : null;
};
- ModalToggleButton은 setShow만 구독하므로 show 값이 변경될 때 리렌더링되지 않음
- 불필요한 렌더링을 방지하여 성능 최적화
보일러플레이트 코드 증가
-> context를 상태와 액션으로 분리하면 코드 복잡 + 유지보수 어려워짐
-> 여러 개 컨텍스트 필요한 경우 Provider가 깊어지면서 Provider Hell 발생
const ThemeContext = createContext();
const UserContext = createContext();
const SettingsContext = createContext();
const App = () => {
return (
<ThemeContext.Provider value="dark">
<UserContext.Provider value={{ name: "John" }}>
<SettingsContext.Provider value={{ sound: true }}>
<MainComponent />
</SettingsContext.Provider>
</UserContext.Provider>
</ThemeContext.Provider>
);
};
- Provider가 많아지면서 코드 가독성이 떨어지고 유지보수가 어려움
- 해결방법 : Custom Hook을 사용하여 Context 묶기
// 해결방법 : Custom Hook을 사용하여 Context 묶기
const AppProviders = ({ children }) => (
<ThemeContext.Provider value="dark">
<UserContext.Provider value={{ name: "John" }}>
<SettingsContext.Provider value={{ sound: true }}>
{children}
</SettingsContext.Provider>
</UserContext.Provider>
</ThemeContext.Provider>
);
const App = () => (
<AppProviders>
<MainComponent />
</AppProviders>
);
- AppProviders라는 HOC (Higher Order Component)를 만들어 중복을 줄임
- App이 더 깔끔해지고 유지보수가 쉬워짐
복잡한 상태 관리가 어려움
-> userReducer 사용하지만 부분 상태 변경을 최적할 방법 부족
-> 객체 상태에서 일부 필드만 변경해도 전체 상태 변경된 것으로 인식하여 리렌더링 발
const UserContext = createContext();
const UserProvider = ({ children }) => {
const [user, setUser] = useState({ name: "Alice", age: 25 });
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
const UserName = () => {
const { user } = useContext(UserContext);
console.log("UserName 렌더링됨");
return <div>이름: {user.name}</div>;
};
const UserAge = () => {
const { user } = useContext(UserContext);
console.log("UserAge 렌더링됨");
return <div>나이: {user.age}</div>;
};
const App = () => (
<UserProvider>
<UserName />
<UserAge />
<button onClick={() => setUser((prev) => ({ ...prev, name: "Bob" }))}>
이름 변경
</button>
</UserProvider>
);
- setUser({ ...prev, name: "Bob" })을 실행하면, age는 변경되지 않았지만 UserAge 컴포넌트도 리렌더링됨
- 객체를 직접 관리하기 때문에 부분 업데이트가 불가능
- 해결 방법: 값 별도로 분리
const UserNameContext = createContext();
const UserAgeContext = createContext();
const UserDispatchContext = createContext();
const UserProvider = ({ children }) => {
const [name, setName] = useState("Alice");
const [age, setAge] = useState(25);
return (
<UserNameContext.Provider value={name}>
<UserAgeContext.Provider value={age}>
<UserDispatchContext.Provider value={{ setName, setAge }}>
{children}
</UserDispatchContext.Provider>
</UserAgeContext.Provider>
</UserNameContext.Provider>
);
};
const UserName = () => {
const name = useContext(UserNameContext);
console.log("UserName 렌더링됨");
return <div>이름: {name}</div>;
};
const UserAge = () => {
const age = useContext(UserAgeContext);
console.log("UserAge 렌더링됨");
return <div>나이: {age}</div>;
};
const App = () => (
<UserProvider>
<UserName />
<UserAge />
<button onClick={() => setName("Bob")}>이름 변경</button>
</UserProvider>
);
- name과 age를 각각 관리하여, name 변경 시 UserAge는 리렌더링되지 않음.
- 불필요한 렌더링 방지 & 성능 최적화.
Context API는 간단한 전역 상태 관리에는 좋지만,
1. 불필요한 리렌더링
2. Provider 중첩 문제 (Provider Hell)
3. 객체 상태 관리 한계
라는 단점이 있으며
해결 방법으로,
1. Context 분리
2. Custom Hook 활용한 Provider 간소화
3. 대규모 상태 관리 Zustand, Redux, Recoil 라이브러리 고려
출처
- https://wikidocs.net/197628#:~:text=Context%20API%EB%8A%94%20%ED%94%84%EB%A1%AD%EC%9D%84,%ED%95%98%EA%B8%B0%20%EC%9C%84%ED%95%9C%20%ED%9B%8C%EB%A5%AD%ED%95%9C%20%EC%84%A0%ED%83%9D%EC%9E%85%EB%8B%88%EB%8B%A4.
- https://f-lab.kr/insight/react-state-management
- https://velog.io/@yrnana/Context-API%EA%B0%80-%EC%A1%B4%EC%9E%AC%ED%95%98%EC%A7%80%EB%A7%8C-%EC%97%AC%EC%A0%84%ED%9E%88-%EC%82%AC%EB%9E%8C%EB%93%A4%EC%9D%B4-redux%EC%99%80-%EC%A0%84%EC%97%AD-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A5%BC-%EC%93%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0
'Front > React' 카테고리의 다른 글
캐싱과 메모이제이션의 차이 (0) | 2025.02.12 |
---|---|
리액트 공부 (2) | 2024.12.10 |
리액트 공부 (2) | 2024.12.10 |
리렌더링이란? 🤷♂️🤷♂️ (0) | 2021.04.09 |
Virtual DOM 이란🧐🧐 (0) | 2021.04.09 |