안녕하세요! 웹 OB 김채현입니다.
React로 개발을 하다 보면, 여러 컴포넌트에서 반복적으로 사용되는 로직을 만나게 됩니다.
그런데 컴포넌트마다 중복된 코드를 계속해서 작성하다 보면 유지보수도 어려워지고 코드도 지저분해질 수 있습니다.
예를 들어, 로딩 상태를 처리하거나 특정 조건에 따라 컴포넌트를 보여줘야 할 때,
일일이 각 컴포넌트에 이 로직을 추가하는 대신 "깔끔하고 효율적인 방법이 없을까?" 하는 고민을 하게 됩니다.
이러한 고민을 해결하기 위해서 중복 로직을 관리하기 위해!
이번 아티클에서는 고차 컴포넌트의 구조와 활용 사례를 살펴보며, 왜 고차 컴포넌트가 유용한지에 대해 함께 알아가 보겠습니다.
💡 고차 컴포넌트가 궁금해요!
고차 컴포넌트(HOC, Higher Order Component)는 컴포넌트 로직의 재사용을 돕는 강력한 패턴입니다.
사용자 정의 훅이 리액트 훅을 기반으로 하고, 리액트에서만 사용될 수 있는 것과 달리,
고차 컴포넌트는 React API의 일부가 아닌 함수적 패턴으로, 자바스크립트의 고차 함수(HOF , Higher Order Function )의 일종입니다.
즉, 컴포넌트를 입력받아 추가적인 props 또는 동작이 있는 새로운 컴포넌트를 반환하는 함수 형태로 구성되며, 자바스크립트의 일급 객체로서의 특징을 활용하기 때문에 React 외의 자바스크립트 환경에서도 널리 사용할 수 있습니다.
고차 컴포넌트는 특정 컴포넌트의 역할이나 기능을 확장하고, 컴포넌트 간 중복 로직을 최소화하는 데 유용합니다.
대표적인 예로 React에서 React.memo가 있는데요, 이는 특정 props가 변경되지 않는 한 컴포넌트를 재렌더링하지 않도록 최적화해 주는 역할을 합니다. 이를 통해 React 개발자들은 성능을 개선하거나 중복된 로직을 단순화할 수 있습니다.
React.memo를 보며 고차 컴포넌트가 무엇인지 조금 더 감을 잡아봅시다!
📝 React.memo란?
React 컴포넌트는 부모 컴포넌트가 리렌더링될 때, 자식 컴포넌트의 props가 변경되지 않았더라도 자동으로 리렌더링되는 특징이 있습니다. 하지만, 이 경우 모든 자식 컴포넌트를 다시 렌더링하는 것은 성능에 영향을 줄 수 있습니다.
React.memo는 이러한 불필요한 렌더링을 방지하기 위해 만들어진 고차 컴포넌트입니다.
import React from 'react';
// React.memo로 최적화된 컴포넌트
const Greeting = React.memo(({ name }) => {
console.log("Greeting 렌더링");
return <p>안녕하세요, {name}님!</p>;
});
// 부모 컴포넌트
const App = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<button onClick={() => setCount(prev => prev + 1)}>
{count}
</button>
<Greeting name="김채현" />
</div>
);
};
export default App;
이 코드에서 자식 컴포넌트(Greeting)를 작성할 때 React.memo를 사용하지 않았다면,
props인 name="김채현"이 변경되지 않았음에도 setCount를 실행해 state를 변경하므로 리렌더링이 발생합니다.
그러나 React.memo를 사용해서 name이 변경되지 않았기 때문에 자식컴포넌트의 리렌더링이 일어나지 않고 있습니다.
바로 React.memo는 컴포넌트를 감싸는 고차 컴포넌트로, 이전 props와 비교해 변경 사항이 없으면 컴포넌트가 렌더링되지 않도록 하기 때문입니다.
이렇게 고차 컴포넌트는 중복되는 로직이나 반복 작업을 최소화하고, 다양한 컴포넌트에 적용할 수 있는 유연한 방법을 제공합니다.
그렇다면 고차 컴포넌트가 필요한 경우는 무엇이 있을까요?
필요한 경우를 보면서 어떻게 작동하는지 알아보겠습니다.
🔍 언제 고차 컴포넌트를 사용하나요?
고차 컴포넌트를 사용하는 이유는 반복적으로 사용하는 로직을 컴포넌트에 쉽게 추가하고, 관리하기 위함으로,
일반적으로 아래와 같은 상황에 사용합니다.
- 코드 재사용 : 여러 컴포넌트에서 동일한 로직을 사용할 때
- 렌더링 변경 : 특정 조건에 따라 컴포넌트의 렌더링 방식을 변경하고자 할 때
- 상태 추상화 : 여러 컴포넌트가 어떤 상태를 공유하고 조작해야 하지만, 해당 상태를 공통 조상으로 올리는 것은 원하지 않을 때
- props 확장 : 원본 컴포넌트의 props를 유지하면서 새로운 props를 추가하여 기능을 확장할 때
- 컴포넌트 생명주기 메서드 접근: 컴포넌트가 마운트되기 전에 데이터를 가져오거나, 언마운트될 때 정리 작업을 수행하려고 할 때
React 16.8 이후로는 Hooks가 도입되면서 컴포넌트 간 상태 로직을 재사용할 수 있는 더 직관적인 대안이 생겼습니다.
커스텀 훅을 통해 로직을 재사용하는 것이 간결하고 가독성이 좋기 때문에, 고차 컴포넌트의 필요성은 많이 줄어든 편이라고 합니다.
🧑🏻💻 고차 컴포넌트를 만들어보자
1️⃣ 고차 함수 만들기
고차 함수는 함수를 인수로 받거나 결과로 반환하는 함수입니다.
map, forEach, reduce 등이 그 예시인데요, 직접 두 값을 더하는 고차 함수를 만들어보았습니다.
const add = (a) => {
return (b) => {
return a + b;
};
};
const result = add(1); // 반환한 함수
const result2 = result(2); // a + b
a=1이라는 정보가 담긴 클로저가 result에 포함되고, result(2)를 호출하면서 이 클로저에 담긴 a=1인 정보를 활용해
1+2라는 결과를 반환하는 구조입니다.
2️⃣ 고차 컴포넌트 만들기
일반적인 컴포넌트를 고차 컴포넌트로 감싸면, 그 고차 컴포넌트는 함수(함수 컴포넌트)를 인수로 받으며,
컴포넌트를 반환하도록 만들어봅시다.
⌛ 로딩 상태를 관리할 때
고차 컴포넌트를 사용해서 로딩 상태에 따라 로딩 메시지를 표시하고, 로딩이 완료되면 컴포넌트를 렌더링할 수 있습니다.
// 로딩 상태를 관리하는 고차 컴포넌트
const withLoading = (WrappedComponent) => {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <p>Loading...</p>;
}
return <WrappedComponent {...props} />;
};
};
const UserProfile = ({ name }) => <p>Welcome, {name}!</p>; // 컴포넌트
const UserProfileWithLoading = withLoading(UserProfile); // 고차 컴포넌트로 감싸기
export default function App() {
return <UserProfileWithLoading isLoading={true} name="김채현" />;
}
이런 방식을 통해 로딩 처리 로직을 여러 컴포넌트에 적용할 수 있습니다.
🪪 인증 요구 기능을 추가할 때
로그인 상태를 확인 후, 인증되지 않은 사용자일 경우 로그인 페이지로 리다이렉트할 수 있도록 할 수 있습니다.
// 인증을 관리하는 고차 컴포넌트
const withAuth = (WrappedComponent) => {
return function WithAuthComponent({ isAuthenticated, ...props }) {
if (!isAuthenticated) {
return <p>로그인이 필요합니다!</p>;
}
return <WrappedComponent {...props} />;
};
};
const Dashboard = () => <p>대시보드에 오신 것을 환영합니다!</p>; // 컴포넌트
const DashboardWithAuth = withAuth(Dashboard); // 고차 컴포넌트로 감싸기
export default function App() {
return <DashboardWithAuth isAuthenticated={false} />;
}
이런 예시 코드처럼 고차 컴포넌트를 사용한다면, 인증된 사용자만 접근할 수 있는 페이지나 기능 제한이 필요한 경우에 사용할 수 있습니다.
⚠️ 고차 컴포넌트를 구현할 때 주의할 점
1. 커스텀 훅이 use로 시작한 것처럼 with로 이름을 시작해야 합니다.
2. 원본 props의 무결성을 유지해 부수 효과를 최소화해야 합니다.
- 기존 props를 임의로 수정하거나 삭제하지 않습니다.
- 새로운 props를 추가할 때는 기존 props와 충돌하지 않도록 합니다.
- spread 연산자(...props)를 사용하여 원본 props를 전달합니다.
3. 여러 개의 고차 컴포넌트를 중첩해서 사용하는 것을 최소화해야 합니다. (과도한 중첩은 Wrapper Hell 문제를 일으켜 코드의 가독성과 디버깅을 어렵게 만들 수 있습니다)
👇🏻👇🏻혹시 props의 무결성을 유지하면서 props확장하는 고차 컴포넌트 코드가 궁금하다면?👇🏻👇🏻
1️⃣ 데이터 포맷팅을 추가하는 경우
// 날짜 형식을 자동으로 포맷팅해주는 고차 컴포넌트
const withDateFormatting = (WrappedComponent) => {
return function WithDateFormat({ date, ...props }) {
const formattedDate = new Date(date).toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
// 기존 date prop은 유지하면서 formattedDate prop 추가
return <WrappedComponent
date={date}
formattedDate={formattedDate}
{...props}
/>;
};
};
// 사용 예시
const EventCard = ({ title, date, formattedDate }) => (
<div>
<h2>{title}</h2>
<p>원본 날짜: {date}</p>
<p>형식화된 날짜: {formattedDate}</p>
</div>
);
const EventCardWithDate = withDateFormatting(EventCard);
<EventCardWithDate
title="회의"
date="2024-03-15"
/>
2️⃣ 스타일과 관련된 props를 추가하는 경우
const withTheme = (WrappedComponent) => {
const themes = {
light: {
backgroundColor: '#ffffff',
color: '#000000'
},
dark: {
backgroundColor: '#1a1a1a',
color: '#ffffff'
}
};
return function WithTheme({ theme = 'light', style, ...props }) {
const themeStyle = themes[theme];
// 기존 style prop과 theme 스타일을 병합
const combinedStyle = {
...themeStyle,
...style
};
return <WrappedComponent
style={combinedStyle}
currentTheme={theme} // 추가 prop
{...props}
/>;
};
};
// 사용 예시
const Card = ({ title, style, currentTheme }) => (
<div style={style}>
<h2>{title}</h2>
<p>현재 테마: {currentTheme}</p>
</div>
);
const ThemedCard = withTheme(Card);
<ThemedCard
title="제목"
theme="dark"
style={{ padding: '20px' }}
/>
💬 Render Props란?
Render Props는 React에서 컴포넌트 간의 코드를 공유하는 기술로, 이 패턴은 동적으로 UI를 조작할 수 있도록 하는 패턴 중 하나입니다.
컴포넌트가 함수 prop을 통해 데이터를 자식에게 전달하도록 구현되며, 자식 컴포넌트는 이 함수를 호출하여 렌더링 방식을 결정합니다.
Render Props는 특히 복잡한 렌더링 로직을 분리하고, 여러 컴포넌트에서 재사용할 수 있는 유연한 방식으로 많이 활용됩니다.
고차 컴포넌트와 마찬가지로 중복된 로직을 쉽게 재사용하고 관리할 수 있게 해줍니다.
// Render Props 패턴을 사용하는 컴포넌트
const MouseTracker = ({ render }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{render(position)}
</div>
);
};
export default function App() {
return (
<MouseTracker
render={(position) => (
<p>마우스 위치: {position.x}, {position.y}</p>
)}
/>
);
}
Render Props 패턴을 적용한 컴포넌트에서는 render라는 prop을 함수로 받아, 마우스 위치 데이터를 전달합니다.
그리고 마우스 위치가 변경될 때마다 position 상태를 업데이트 하며 render 함수를 호출하여 화면에 표시합니다.
따라서 MouseTracker의 상태 변화에 따라 동적으로 UI를 업데이트할 수 있습니다.
일반적으로 Render Props 패턴은 아래와 같은 상황에서 사용합니다.
- 코드 재사용 : 여러 컴포넌트에서 공유되는 동작이 있을 때
- Wrapper Hell 회피 : 여러 개의 고차 컴포넌트로 구현될 때
- 동적 구성 : 공유 동작을 동적으로 구성해야 할 때, props에 따라 동작을 다르게 할 때
- 상태 공유 : 컴포넌트 간에 서로의 상태를 공유하거나 영향을 주고 싶을 때
- 조건부 렌더링 : 조건에 따라 렌더링하거나 추가 요소를 추가하고 싶을 때
🎯 Render Props와 고차 컴포넌트의 비교
Render Props | 고차 컴포넌트 | |
사용 방식 | render와 같은 함수형 prop을 통해 UI 렌더링 제어 | 컴포넌트를 인수로 받아 새로운 컴포넌트를 반환 |
UI 제어 방식 | 자식 컴포넌트에서 렌더링 방식을 결정 | 새로운 컴포넌트가 반환되어 독립적인 역할을 수행 |
공통 로직 처리 방식 | 중첩 구조가 심해질 수 있음 | Wrapper Hell 문제 발생 가능 |
예시 | Mouse 위치 추적, 이벤트 핸들링 등 | 로딩 관리, 인증 처리, 데이터 전처리 등 |
복잡한 렌더링 로직이 필요할 때는 Render Props가 적합하고,
상태 관리나 조건부 렌더링 등의 기능을 여러 컴포넌트에 일괄 적용하려면 고차 컴포넌트가 적합합니다.
💡 Render Props와 고차 컴포넌트의 조합
Render Props와 고차 컴포넌트는 서로 보완적인 패턴으로 함께 사용될 수도 있습니다.
예를 들어, 데이터 fetching은 고차 컴포넌트로 처리하고, 이 데이터를 받아 UI를 조정하는 부분은 Render Props로 처리할 수 있습니다. Render Props와 HOC의 결합은 복잡한 로직을 효율적으로 분리할 수 있는 강력한 방법이 될 수 있습니다.
// 고차 컴포넌트로 데이터 fetching 로직을 추가
const withDataFetching = (WrappedComponent, url) => {
return function DataFetchingComponent(props) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then((response) => response.json())
.then((data) => setData(data));
}, [url]);
return <WrappedComponent data={data} {...props} />;
};
};
// Render Props로 데이터 렌더링 제어
const DataDisplay = ({ data }) => {
return data ? <p>데이터: {JSON.stringify(data)}</p> : <p>Loading...</p>;
};
// 조합하여 사용
const EnhancedDataDisplay = withDataFetching(DataDisplay, 'https://api.example.com/data');
그러나 React 16.8에서 소개된 Hooks가 컴포넌트 간에 상태 로직을 재사용하는 방법을 제공하는데, 이로 인해 고차 컴포넌트와 Render Props의 필요성이 줄었습니다. 따라서 최신 React 코드베이스에서는 이러한 패턴 대신 Hooks를 더 자주 사용하는 것을 볼 수 있습니다.
❓커스텀 훅 vs 고차 컴포넌트
단순히 useEffect, useState와 같이 React에서 제공하는 Hook으로만 공통 로직을 격리할 수 있다면 커스텀 훅을 사용하는 것이 좋습니다.
커스텀 훅은 그 자체로는 렌더링에 영향을 미치지 못하기 때문에 사용이 제한적이고 부수 효과가 비교적 제한적입니다.
대부분의 고차 컴포넌트는 렌더링에 영향을 미치는 로직이 존재해서 비교적 결과를 예측하기 어렵습니다.
따라서 단순히 컴포넌트들에 동일한 로직으로 값을 제공하거나 작동시키고 싶을 때는 커스텀 훅을 사용해야 합니다.
공통 컴포넌트를 노출하는 것이 좋은 경우나 특정 에러가 발생했을 때 에러가 발생했음을 알릴 수 있는 컴포넌트를 노출하는 경우가 있습니다. 이러한 작업은 렌더링 결과물에 영향을 미치지 않는 커스텀 훅만으로는 표현하기 어렵습니다.
이러한 중복 처리는 커스텀 훅을 사용하는 프로젝트에 전체적으로 이루어지기 때문에 커스텀 훅보다는 고차 컴포넌트를 사용하는 것이 좋습니다.
즉, 고차 컴포넌트는 공통화된 렌더링 조직을 처리하기에 좋습니다.
이렇게 중복 로직을 해결하는 다양한 방법을 알아보았습니다!
앞으로 더 효율적인 코드를 작성할 수 있도록 함께 노력하면서 성장합시다.🌊🌊
읽어주셔서 감사합니다 :)
참고한 글
'3주차' 카테고리의 다른 글
컴포넌트 순수하게 유지하기 (0) | 2024.11.02 |
---|---|
명령형 프로그래밍과 선언형 프로그래밍 (0) | 2024.11.02 |
[React] children prop과 친해지자 (0) | 2024.11.02 |
React Dev Tools? 이것 뭐예요~?? (0) | 2024.11.02 |
DOM과 React 랜더링 (0) | 2024.11.02 |