- Published on
리액트 컴포넌트 설계 기본 기법
- Authors
- Name
- 박유상
리액트에서는 다양한 컴포넌트 설계 기법이 존재하며, 각각의 기법은 특정 요구사항에 맞는 구조와 접근 방식을 제공합니다. 이러한 기법들은 주로 컴포넌트 간의 역할 분리, 상태 관리, 재사용성을 위해 도입되었습니다. 이 글에서는 몇 가지 주요 설계 기법을 살펴보고, 각각의 장단점을 탐구합니다.
Presentational & Container Components
Presentational Components는 화면 표시(UI)만 담당하며, 스타일링과 UI 구성에 중점을 둡니다. 반면, Container Components는 비즈니스 로직과 상태 관리를 담당합니다.
장점
- 관심사의 분리: UI와 로직이 명확히 분리되어 있어 코드의 가독성과 유지보수성이 향상됩니다.
- 테스트 용이성: 로직과 UI를 독립적으로 테스트할 수 있습니다.
- UI 재사용성: UI 컴포넌트를 다른 프로젝트에서도 손쉽게 활용할 수 있습니다.
단점
- 추가적인 복잡성: Functional Component와 Hooks의 등장 이후, 이러한 패턴은 권장되지 않습니다.
- 현재는 필요한 로직을 Custom Hook으로 추출하는 방식이 선호됩니다.
- 컨테이너의 비대화: 컨테이너 컴포넌트가 과도하게 커질 가능성이 있습니다.
// Presentational Component: UI만 담당
const CounterDisplay = ({ count, onIncrement, onDecrement }) => (
<div>
<button onClick={onDecrement}>-</button>
<span>{count}</span>
<button onClick={onIncrement}>+</button>
</div>
);
// Container Component: 상태와 로직 관리
const CounterContainer = () => {
const [count, setCount] = React.useState(0);
const increment = () => setCount((prev) => prev + 1);
const decrement = () => setCount((prev) => prev - 1);
return (
<CounterDisplay count={count} onIncrement={increment} onDecrement={decrement} />
);
};
Render Props
Render Props는 컴포넌트가 자식에게 렌더링 로직을 전달하는 패턴입니다. render 또는 children 형태의 함수 props를 통해 UI를 동적으로 구성하며, 경우에 따라 데이터도 함께 전달합니다.
장점
- 로직 재사용성: 자식 컴포넌트가 원하는 방식으로 UI를 렌더링할 수 있습니다.
- 유연성: UI 구성을 동적으로 변경할 수 있습니다.
const MouseTracker = ({ children }) => {
const [position, setPosition] = React.useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{children(position)}
</div>
);
};
Compound Components
Compound Components는 부모 컴포넌트가 상태와 로직을 제공하고, 자식 컴포넌트들이 이를 활용하도록 설계된 패턴입니다. React Context를 활용하여 컴포넌트 상태를 통합하기에 적합합니다.
장점
- 구조적 일관성: 부모 컴포넌트에서 상태와 로직을 통합 관리.
- 유연한 자식 컴포넌트 사용: 자식 컴포넌트들이 부모의 상태를 자유롭게 사용할 수 있습니다.
import { createContext, useContext, useState } from "react";
const CounterContext = createContext();
function Counter({ children }) {
const [count, setCount] = useState(0);
const increase = () => setCount((c) => c + 1);
const decrease = () => setCount((c) => c - 1);
return (
<CounterContext.Provider value={{ count, increase, decrease }}>
{children}
</CounterContext.Provider>
);
}
function Count() {
const { count } = useContext(CounterContext);
return <span>{count}</span>;
}
function Label({ children }) {
return <span>{children}</span>;
}
function Increase() {
const { increase } = useContext(CounterContext);
return <button onClick={increase}>+</button>;
}
function Decrease() {
const { decrease } = useContext(CounterContext);
return <button onClick={decrease}>-</button>;
}
Counter.Count = Count;
Counter.Label = Label;
Counter.Increase = Increase;
Counter.Decrease = Decrease;
export default Counter;
Control Props Pattern
Control Props Pattern은 부모 컴포넌트가 상태와 핸들러를 제어하며, 이를 자식 컴포넌트에 전달하는 패턴입니다. Presentational & Container Component와 비슷하지만, UI와 로직을 구분하기보다는 부모와 자식 간의 관계에 중점을 둡니다.
const Counter = ({ value, onIncrement, onDecrement }) => (
<div>
<button onClick={onDecrement}>-</button>
<span>{value}</span>
<button onClick={onIncrement}>+</button>
</div>
);
const App = () => {
const [countA, setCountA] = React.useState(0);
const [countB, setCountB] = React.useState(10);
const incrementA = () => setCountA((prev) => prev + 1);
const decrementA = () => setCountA((prev) => prev - 1);
const incrementB = () => setCountB((prev) => prev + 2);
const decrementB = () => setCountB((prev) => prev - 2);
return (
<div>
<Counter value={countA} onIncrement={incrementA} onDecrement={decrementA} />
<Counter value={countB} onIncrement={incrementB} onDecrement={decrementB} />
</div>
);
};
export default App;
Custom Hook
Custom Hook은 로직 재사용성을 극대화하기 위해 리액트에서 권장되는 패턴입니다. 재사용 가능한 로직을 hook으로 분리하여 유지보수성을 높이고, 컴포넌트에서 중복된 코드를 줄일 수 있습니다.
장단
- 로직 캡슐화: 복잡한 로직을 간결하게 분리 가능.
- 재사용성: 동일한 로직을 다양한 컴포넌트에서 반복 사용 가능.
import { useState } from "react";
const useCounter = (initialValue = 0) => {
const [count, setCount] = useState(initialValue);
const increment = () => setCount((prev) => prev + 1);
const decrement = () => setCount((prev) => prev - 1);
return { count, increment, decrement };
};
export default useCounter;
Custom Hook with Reducer
useReducer를 사용하여 상태와 로직을 관리하면, 복잡한 상태 전이를 더 효율적으로 처리할 수 있습니다. 이는 Redux의 간소화된 형태로 볼 수 있습니다.
코드 예제
const counterReducer = (state, action) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error("Unhandled action type");
}
};
const useCounter = (initialValue = 0) => {
const [state, dispatch] = React.useReducer(counterReducer, { count: initialValue });
const increment = () => dispatch({ type: "increment" });
const decrement = () => dispatch({ type: "decrement" });
return { count: state.count, increment, decrement };
};
마무리
컴포넌트 설계의 핵심은 특정 패턴을 맹목적으로 따르기보다, 프로젝트의 요구사항과 규모에 맞게 유연하고 유지보수 가능한 구조를 만드는 것이다. 역할을 명확히 나누고, 코드의 확장성과 유지보수성을 높이는 것이야말로 성공적인 리액트 개발의 핵심이다.