한국어

React의 컴포넌트 아키텍처를 심층적으로 살펴보고, Composition과 상속을 비교합니다. React가 Composition을 선호하는 이유와 HOC, Render Props, Hooks와 같은 패턴을 탐구하여 확장 가능하고 재사용 가능한 컴포넌트를 구축하는 방법을 알아보세요.

React 컴포넌트 아키텍처: Why Composition Triumphs Over Inheritance

소프트웨어 개발에서 아키텍처는 가장 중요합니다. 코드를 구성하는 방식은 확장성, 유지 관리성 및 재사용 가능성을 결정합니다. React를 사용하는 개발자의 경우, 가장 기본적인 아키텍처 결정 중 하나는 컴포넌트 간에 로직과 UI를 공유하는 방식에 관한 것입니다. 이는 React의 컴포넌트 기반 세상에 맞춰 재해석된 객체 지향 프로그래밍의 고전적인 논쟁, 즉 Composition vs. Inheritance로 이어집니다.

Java 또는 C++와 같은 고전적인 객체 지향 언어를 사용한 경험이 있다면 상속이 자연스러운 첫 번째 선택으로 느껴질 수 있습니다. 'is-a' 관계를 만드는 데 강력한 개념입니다. 그러나 공식 React 설명서에서는 명확하고 강력한 권장 사항을 제공합니다. "Facebook에서는 수천 개의 컴포넌트에서 React를 사용하며 컴포넌트 상속 계층 구조를 만드는 것을 권장하는 사용 사례를 찾지 못했습니다."

이 게시물에서는 이러한 아키텍처 선택에 대한 포괄적인 탐구를 제공합니다. React 컨텍스트에서 상속과 Composition이 의미하는 바를 풀고, Composition이 관용적이고 우수한 접근 방식인 이유를 설명하고, 고차 컴포넌트에서 현대 Hooks에 이르기까지 강력한 패턴을 살펴보고, 이를 통해 Composition을 전 세계 사용자를 위한 강력하고 유연한 애플리케이션을 구축하기 위한 개발자의 가장 친한 친구로 만들 것입니다.

오래된 경비대를 이해하기: 상속이란 무엇인가?

상속은 객체 지향 프로그래밍(OOP)의 핵심 기둥입니다. 새 클래스(하위 클래스 또는 자식)가 기존 클래스(슈퍼클래스 또는 부모)의 속성과 메서드를 얻을 수 있도록 합니다. 이렇게 하면 밀접하게 결합된 'is-a' 관계가 생성됩니다. 예를 들어, GoldenRetrieverDog이고, DogAnimal입니다.

React가 아닌 컨텍스트에서의 상속

개념을 확실히 하기 위해 간단한 JavaScript 클래스 예를 살펴보겠습니다.

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Calls the parent constructor
    this.breed = breed;
  }

  speak() { // Overrides the parent method
    console.log(`${this.name} barks.`);
  }

  fetch() {
    console.log(`${this.name} is fetching the ball!`);
  }
}

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); // Output: "Buddy barks."
myDog.fetch(); // Output: "Buddy is fetching the ball!"

이 모델에서 Dog 클래스는 자동으로 Animal에서 name 속성과 speak 메서드를 가져옵니다. 또한 자체 메서드(fetch)를 추가하고 기존 메서드를 재정의할 수도 있습니다. 이렇게 하면 엄격한 계층 구조가 생성됩니다.

React에서 상속이 실패하는 이유

이 'is-a' 모델은 일부 데이터 구조에 적합하지만 React의 UI 컴포넌트에 적용하면 심각한 문제가 발생합니다.

이러한 문제로 인해 React 팀은 보다 유연하고 강력한 패러다임인 Composition을 중심으로 라이브러리를 설계했습니다.

React 방식 수용: Composition의 힘

Composition은 'has-a' 또는 'uses-a' 관계를 선호하는 설계 원칙입니다. 컴포넌트가 다른 컴포넌트 대신 다른 컴포넌트를 가지거나 해당 기능을 사용합니다. 컴포넌트는 레고 블록처럼 엄격한 계층 구조에 갇히지 않고 다양한 방식으로 결합하여 복잡한 UI를 만들 수 있는 빌딩 블록으로 취급됩니다.

React의 Composition 모델은 매우 다재다능하며, 몇 가지 주요 패턴으로 나타납니다. 가장 기본적인 것부터 가장 현대적이고 강력한 것까지, 이를 살펴보겠습니다.

기술 1: props.children을 사용한 Containment

가장 간단한 형태의 Composition은 Containment입니다. 이는 컴포넌트가 일반 컨테이너 또는 '상자' 역할을 하고 해당 콘텐츠가 상위 컴포넌트에서 전달되는 경우입니다. React에는 이를 위한 특별한 기본 제공 prop이 있습니다. props.children입니다.

일관된 테두리와 그림자가 있는 모든 콘텐츠를 래핑할 수 있는 Card 컴포넌트가 필요하다고 상상해 보십시오. 상속을 통해 TextCard, ImageCardProfileCard 변형을 만드는 대신 하나의 일반 Card 컴포넌트를 만듭니다.

// Card.js - A generic container component
function Card(props) {
  return (
    <div className="card">
      {props.children}
    </div>
  );
}

// App.js - Using the Card component
function App() {
  return (
    <div>
      <Card>
        <h1>Welcome!</h1>
        <p>This content is inside a Card component.</p>
      </Card>

      <Card>
        <img src="/path/to/image.jpg" alt="An example image" />
        <p>This is an image card.</p>
      </Card>
    </div>
  );
}

여기서 Card 컴포넌트는 포함된 내용을 알거나 신경 쓰지 않습니다. 단순히 래퍼 스타일을 제공합니다. 여는 태그와 닫는 <Card> 태그 사이의 콘텐츠는 자동으로 props.children으로 전달됩니다. 이는 분리 및 재사용성의 훌륭한 예입니다.

기술 2: Props를 사용한 특수화

경우에 따라 컴포넌트에 다른 컴포넌트가 채워야 하는 여러 개의 '구멍'이 필요합니다. props.children을 사용할 수 있지만, 컴포넌트를 일반 prop으로 전달하는 것이 더 명시적이고 구조화된 방법입니다. 이 패턴은 종종 특수화라고 합니다.

Modal 컴포넌트를 생각해 보세요. 모달에는 일반적으로 제목 섹션, 콘텐츠 섹션 및 작업 섹션(예: "확인" 또는 "취소"와 같은 버튼 포함)이 있습니다. 이러한 섹션을 prop으로 허용하도록 Modal을 설계할 수 있습니다.

// Modal.js - A more specialized container
function Modal(props) {
  return (
    <div className="modal-backdrop">
      <div className="modal-content">
        <div className="modal-header">{props.title}</div>
        <div className="modal-body">{props.body}</div>
        <div className="modal-footer">{props.actions}</div>
      </div>
    </div>
  );
}

// App.js - Using the Modal with specific components
function App() {
  const confirmationTitle = <h2>Confirm Action</h2>;
  const confirmationBody = <p>Are you sure you want to proceed with this action?</p>;
  const confirmationActions = (
    <div>
      <button>Confirm</button>
      <button>Cancel</button>
    </div>
  );

  return (
    <Modal
      title={confirmationTitle}
      body={confirmationBody}
      actions={confirmationActions}
    />
  );
}

이 예에서 Modal은 고도로 재사용 가능한 레이아웃 컴포넌트입니다. title, bodyactions에 대해 특정 JSX 요소를 전달하여 특수화합니다. 이는 ConfirmationModalWarningModal 하위 클래스를 만드는 것보다 훨씬 유연합니다. 필요에 따라 다른 콘텐츠로 Modal을 Composition하면 됩니다.

기술 3: 고차 컴포넌트(HOC)

데이터 가져오기, 인증 또는 로깅과 같은 비 UI 로직을 공유하기 위해 React 개발자는 역사적으로 고차 컴포넌트(HOC)라는 패턴을 사용했습니다. 현대 React에서는 Hooks로 대체되었지만, React의 Composition 스토리에서 핵심적인 진화 단계를 나타내며 많은 코드베이스에 여전히 존재하므로 이를 이해하는 것이 중요합니다.

HOC는 컴포넌트를 인수로 사용하고 새롭게 향상된 컴포넌트를 반환하는 함수입니다.

컴포넌트가 업데이트될 때마다 컴포넌트의 프롭을 기록하는 withLogger라는 HOC를 만들어 보겠습니다. 이는 디버깅에 유용합니다.

// withLogger.js - The HOC
import React, { useEffect } from 'react';

function withLogger(WrappedComponent) {
  // It returns a new component...
  return function EnhancedComponent(props) {
    useEffect(() => {
      console.log('Component updated with new props:', props);
    }, [props]);

    // ... that renders the original component with the original props.
    return <WrappedComponent {...props} />;
  };
}

// MyComponent.js - A component to be enhanced
function MyComponent({ name, age }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>You are {age} years old.</p>
    </div>
  );
}

// Exporting the enhanced component
export default withLogger(MyComponent);

withLogger 함수는 MyComponent를 래핑하여 MyComponent의 내부 코드를 수정하지 않고도 새로운 로깅 기능을 제공합니다. 이 동일한 HOC를 다른 컴포넌트에 적용하여 동일한 로깅 기능을 제공할 수 있습니다.

HOC의 과제:

기술 4: Render Props

Render Prop 패턴은 HOC의 몇 가지 단점을 해결하기 위해 등장했습니다. 보다 명시적인 방식으로 로직을 공유하는 방법을 제공합니다.

render prop이 있는 컴포넌트는 함수를 prop으로 사용하고(일반적으로 render라는 이름) 해당 함수를 호출하여 렌더링할 내용을 결정하고, 모든 상태 또는 로직을 인수로 전달합니다.

마우스의 X 및 Y 좌표를 추적하고 이를 사용하려는 모든 컴포넌트에서 사용할 수 있도록 하는 MouseTracker 컴포넌트를 만들어 보겠습니다.

// MouseTracker.js - Component with a render prop
import React, { useState, useEffect } from 'react';

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (event) => {
    setPosition({ x: event.clientX, y: event.clientY });
  };

  useEffect(() => {
    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);

  // Call the render function with the state
  return render(position);
}

// App.js - Using the MouseTracker
function App() {
  return (
    <div>
      <h1>Move your mouse around!</h1>
      <MouseTracker
        render={mousePosition => (
          <p>The current mouse position is ({mousePosition.x}, {mousePosition.y})</p>
        )}
      />
    </div>
  );
}

여기서 MouseTracker는 마우스 이동을 추적하기 위한 모든 로직을 캡슐화합니다. 자체적으로는 아무것도 렌더링하지 않습니다. 대신, 렌더링 로직을 render prop에 위임합니다. 이는 HOC보다 명시적입니다. JSX 내에서 mousePosition 데이터가 정확히 어디에서 오는지 확인할 수 있기 때문입니다.

children prop은 이 패턴의 일반적이고 우아한 변형인 함수로도 사용할 수 있습니다.

// Using children as a function
<MouseTracker>
  {mousePosition => (
    <p>The current mouse position is ({mousePosition.x}, {mousePosition.y})</p>
  )}
</MouseTracker>

기술 5: Hooks(현대적이고 선호되는 접근 방식)

React 16.8에 도입된 Hooks는 React 컴포넌트를 작성하는 방식을 혁신했습니다. 이를 통해 함수형 컴포넌트에서 상태 및 기타 React 기능을 사용할 수 있습니다. 가장 중요한 것은, 사용자 지정 Hooks는 컴포넌트 간에 상태 로직을 공유하기 위한 가장 우아하고 직접적인 솔루션을 제공합니다.

Hooks는 HOC 및 Render Props의 문제를 훨씬 더 깔끔하게 해결합니다. MouseTracker 예제를 useMousePosition이라는 사용자 지정 Hook으로 리팩터링해 보겠습니다.

// hooks/useMousePosition.js - A custom Hook
import { useState, useEffect } from 'react';

export function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({ x: event.clientX, y: event.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // Empty dependency array means this effect runs only once

  return position;
}

// DisplayMousePosition.js - A component using the Hook
import { useMousePosition } from './hooks/useMousePosition';

function DisplayMousePosition() {
  const position = useMousePosition(); // Just call the hook!

  return (
    <p>
      The mouse position is ({position.x}, {position.y})
    </p>
  );
}

// Another component, maybe an interactive element
import { useMousePosition } from './hooks/useMousePosition';

function InteractiveBox() {
  const { x, y } = useMousePosition();

  const style = {
    position: 'absolute',
    top: y - 25, // Center the box on the cursor
    left: x - 25,
    width: '50px',
    height: '50px',
    backgroundColor: 'lightblue',
  };

  return <div style={style} />;
}

이는 엄청난 개선입니다. '래퍼 지옥'도, prop 이름 충돌도, 복잡한 렌더 prop 함수도 없습니다. 로직은 재사용 가능한 함수(useMousePosition)로 완전히 분리되었으며, 모든 컴포넌트는 명확한 한 줄의 코드만으로 해당 상태 로직에 '연결'할 수 있습니다. 사용자 지정 Hooks는 현대 React에서 Composition의 궁극적인 표현으로, 재사용 가능한 로직 블록의 자체 라이브러리를 구축할 수 있도록 합니다.

간단한 비교: React에서 Composition vs. Inheritance

React 컨텍스트에서 주요 차이점을 요약하면 다음과 같은 직접 비교가 있습니다.

측면 상속(React에서 Anti-Pattern) Composition(React에서 선호)
<strong>관계</strong> 'is-a' 관계입니다. 특수화된 컴포넌트는 기본 컴포넌트의 a 버전입니다. 'has-a' 또는 'uses-a' 관계입니다. 복잡한 컴포넌트는 더 작은 컴포넌트를 가지거나 공유 로직을 사용합니다.
<strong>결합</strong> <strong>높음.</strong> 자식 컴포넌트는 부모의 구현에 밀접하게 결합됩니다. <strong>낮음.</strong> 컴포넌트는 독립적이며 수정 없이 다른 컨텍스트에서 재사용할 수 있습니다.
<strong>유연성</strong> <strong>낮음.</strong> 엄격한 클래스 기반 계층 구조로 인해 다른 컴포넌트 트리에 로직을 공유하기가 어렵습니다. <strong>높음.</strong> 로직과 UI는 마치 빌딩 블록처럼 무수히 많은 방식으로 결합하고 재사용할 수 있습니다.
<strong>코드 재사용성</strong> 미리 정의된 계층 구조로 제한됩니다. '바나나'만 원할 때 전체 '고릴라'를 얻습니다. 우수합니다. 작고, 집중된 컴포넌트와 Hooks는 전체 애플리케이션에서 사용할 수 있습니다.
<strong>React 관용구</strong> React 공식 팀에서 권장하지 않습니다. React 애플리케이션을 구축하기 위한 권장되고 관용적인 접근 방식입니다.

결론: Composition으로 생각하십시오

Composition과 상속 간의 논쟁은 소프트웨어 설계의 기본적인 주제입니다. 상속은 고전적인 OOP에서 자리를 잡았지만 UI 개발의 동적이고 컴포넌트 기반 특성으로 인해 React에 적합하지 않습니다. 라이브러리는 근본적으로 Composition을 수용하도록 설계되었습니다.

Composition을 선호함으로써 다음과 같은 이점을 얻을 수 있습니다.

전 세계 React 개발자로서 Composition을 마스터하는 것은 모범 사례를 따르는 것만이 아니라 React를 강력하고 생산적인 도구로 만드는 핵심 철학을 이해하는 것입니다. 작고, 집중된 컴포넌트를 만드는 것부터 시작하십시오. 일반 컨테이너에는 props.children을 사용하고 특수화에는 props를 사용하십시오. 로직을 공유하려면 먼저 사용자 지정 Hooks를 사용하십시오. Composition으로 생각함으로써 시간이 지나도 변치 않는 우아하고, 강력하며, 확장 가능한 React 애플리케이션을 구축하는 데 도움이 될 것입니다.