본문 바로가기

FrontEnd

리액트 디자인 패턴 : 선언적 컴포넌트 설계 (declarative component design)

반응형

어떤 컴포넌트가 선언적인가?

어떻게(how) 보여줄까가 아닌 무엇을(what) 보여줄까

아래 글의 도입부에서 명령형 컴포넌트와 선언형 컴포넌트를 다룹니다.
https://tech.kakaopay.com/post/react-query-2/

React Query와 함께 Concurrent UI Pattern을 도입하는 방법 | 카카오페이 기술 블로그

카카오페이에서 React Query를 활용하여 Concurrent UI Pattern을 도입한 사례에 대해 소개합니다. 이 글은 연작 중 2편에 해당합니다. 1편: 카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유, 2

tech.kakaopay.com

아래는 명령형 컴포넌트입니다.

import { useState, useEffect } from 'react';
const ImperativeComponent = () => {
  const [ isLoading, setIsLoading ] = useState(false);
  const [ data, setData ] = useState();
  const [ error, setError ] = useState();
  useEffect(() => {
    !async () => {
      try {
        setIsLoading(true);
        const { json } = await fetch(URL);
        setData(json());
        setError(undefined);
        setIsLoading(false);
      } catch(e) {
        setData(undefined);
        setError(e);
        setIsLoading(false);
      }
    }();
  }, []);
  if (isLoading) {
    return <Spinner/>
  }
  if (error) {
    return <ErrorMessage error={error}/>
  }
  return <DataView data={data}/>;
}
export default ImperativeComponent;

아래는 선언형 컴포넌트입니다.

import { Suspense } from 'react';
const User = () => {
  return (
    // UserProfile에서 비동기 데이터를 로딩하고 있는 경우
    // Suspense의 fallback을 통해 Spinner를 보여줍니다.
    <Suspense fallback={<Spinner/>}>
      <UserProfile/>
    </Suspense>
  );
}
const UserProfile = () => {
  // userProfileRepository는 Suspense를 지원하는 "특별한 객체"
  const { data } = userProfileRepository();
  return (
    // 마치 데이터가 "이미 존재하는 것처럼" 사용합니다.
    <span> {data.name} / {data.birthDay} </span>
  );
}
export default User;
import { Component } from 'react';
class MyCustomErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError(error) {
    // 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트 합니다.
    return { hasError: true };
  }
    componentDidCatch(error, errorInfo) {
    // 에러 리포팅 서비스에 에러를 기록할 수도 있습니다.
    logErrorToMyService(error, errorInfo);
  }
  render() {
    if (this.state.hasError) {
      // 폴백 UI를 커스텀하여 렌더링할 수 있습니다.
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}
const App = () => {
  return (
    <MyCustomErrorBoundary>
      <MyApp/>
    </MyCustomErrorBoundary>
  )
}
export default App;

대체 차이가 무엇일까? ~일 때는 ~를 보여주는건 동일한게 아닌가?
곰곰이 생각해보니 아래와 같이 추상화 할 수 있겠네요.
즉 if else를 컴포넌트 내부에서 다루고,
개발자는 그 상황에 보여줄 무엇에만 집중하여 컴포넌트로 개발합니다.

      <DeclarativeComponent
        case1="case1"
        case2="case2"
        case3="case3"
      >
        hello world
      </DeclarativeComponent>

즉, 어떤 상황엔 어떤 컴포넌트를 보여줘라!를 프롭스로 명시적으로 나타내는 것입니다!

당연히 명령형 구현이 필요하지만,
API는 선언적이 됩니다!
아래는 위 컴포넌트 명령형 구현입니다.

function randomIntFromInterval(min, max) {
  // min and max included
  return Math.floor(Math.random() * (max - min + 1) + min);
}

const useLogic = () => randomIntFromInterval(1, 4);
export default function DeclarativeComponent({
  case1,
  case2,
  case3,
  children
}) {
  const internalLogic = useLogic();
  switch (internalLogic) {
    case 1: {
      return <>{case1}</>;
    }
    case 2: {
      return <>{case2}</>;
    }
    case 3: {
      return <>{case3}</>;
    }
    default: {
      return <>{children}</>;
    }
  }
}

리액트의 꽃인 props를 통한 컴포넌트 합성의 묘미가 느껴지시죠....?


아래는 제가 프로젝트에서 사용한 방법의 예시입니다

export default function DeclarativeLayout({
  tabletView,  
  children
}) {
  const isTablet = useMode();
  switch (isTablet) {
    case true: {
      return <TabletLayout>{tabletView}</TabletLayout>;
    }
    default: {
      return <MobileLayout>{children}</MobileLayout>;
    }
  }
}

사실 레이아웃도 같이 주입해주는게 좋습니다. 그냥 이해를 위해 저렇게 적어뒀다 생각해주세요

참고 :

https://itchallenger.tistory.com/485

리액트 디자인 패턴 : Container pattern (컨테이너 패턴)

데이터 가져오기, 인증, 레이아웃 제공 등과 같은 커스텀 기능을 재사용하는 선언적 방법을 구현할 수 있습니다. 컨테이너 패턴이란? 커스텀 기능을 제공하고 children prop으로 전달된 React 노드를

itchallenger.tistory.com

반응형