logo
Published on

리액트 컴포넌트 설계 기본 기법

Authors
  • avatar
    Name
    박유상
    Twitter

리액트에서는 다양한 컴포넌트 설계 기법이 존재하며, 각각의 기법은 특정 요구사항에 맞는 구조와 접근 방식을 제공합니다. 이러한 기법들은 주로 컴포넌트 간의 역할 분리, 상태 관리, 재사용성을 위해 도입되었습니다. 이 글에서는 몇 가지 주요 설계 기법을 살펴보고, 각각의 장단점을 탐구합니다.

  1. Presentational & Container Components
  2. Render Props
  3. Custom Hook
  4. Custom Hook with Reducer
  5. 마무리

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 };
};

마무리

컴포넌트 설계의 핵심은 특정 패턴을 맹목적으로 따르기보다, 프로젝트의 요구사항과 규모에 맞게 유연하고 유지보수 가능한 구조를 만드는 것이다. 역할을 명확히 나누고, 코드의 확장성과 유지보수성을 높이는 것이야말로 성공적인 리액트 개발의 핵심이다.