React VAC 패턴

Jemin·2023년 7월 26일
0

개발 지식

목록 보기
26/51
post-thumbnail

Presentaitional and Container 패턴

React 컴포넌트에 관해서

이전에 컴포넌트 리팩토링을 하면서 학습하고 정리했던 Presentational & Container 디자인 패턴에 대한 글이다.

P&C 패턴은 React 개발에서 UI 컴포넌트와 상태 및 로직을 분리하는 디자인 패턴이다. 이 패턴은 기능과 UI를 분리하여 컴포넌트를 더 재사용 가능하고 유지 보수하기 쉽도록 만드는 데 목적이 있다.

그러나 최근 React에서는 함수형 컴포넌트와 React Hooks와 Custom Hooks의 도입으로 인해 P&C 패턴을 사용하지 않는 추세가 있다. 이러한 이유로 인해 해당 패턴의 사용이 줄어들었다.

  • 함수형 컴포넌트와 Hooks의 간편함: 함수형 컴포넌트는 클래스형 컴포넌트보다 더 간결하고 이해하기 쉽게 작성할 수 있다. 또한, Hooks를 사용하면 상태와 생명 주기를 관리하는데 필요한 로직을 컴포넌트 외부로 분리할 필요 없이 함수 내에서 처리할 수 있다.

  • 코드 구조 단순화: P&C 패턴을 사용하면 컴포넌트를 두 가지 타입으로 분리해야 하기 때문에 코드 구조가 더 복잡해질 수 있다. 함수형 컴포넌트와 Hooks를 사용하면 컴포넌트가 단일 함수로 작성되어 코드 구조를 단순화할 수 있다.

  • Hooks의 상태 관리 기능: Hooks 중에서도 useState, useEffect와 같은 상태 관리 기능을 사용하면 컴포넌트에서 상태를 직접 다룰 수 있기 때문에 P&C 패턴으로 인해 추가적인 컨테이너 컴포넌트를 만들 필요가 없다.

  • 커스텀 Hooks의 활용: Hooks를 사용하여 커스텀 Hooks를 작성하면 여러 컴포넌트에서 로직을 재사용할 수 있다. 이를 통해 P&C 패턴보다 더 모듈화된 코드를 작성할 수 있다.

따라서 최근에는 함수형 컴포넌트와 Hooks를 사용하여 P&C 패턴보다 더 간결하고 유연한 React 컴포넌트를 작성하는 추세다.

VAC 패턴

VAC 패턴은 View Asset Component의 약자로 렌더링이 필요한 JSX와 스타일을 관리하는 컴포넌트를 의미한다.

프론트엔드를 개발하는 것은 크게 세 가지로 구분할 수 있다.

  1. 사용자의 상호작용 처리 UI 기능개발(JS)

  2. 데이터나 상호작용 결과를 시각화하는 렌더링 처리(마크업, CSS)

  3. 비즈니스 로직, UI 기능, 렌더링 처리의 통합(React, Redux)

회사의 규모가 커질 수록 2번에 해당하는 UI개발(마크업 개발) 영역과 1, 3번에 해당하는 FE개발 영역으로 역할이 나뉘어 있다.

하지만 React에서는 컴포넌트를 개발할 때 JSX로 렌더링을 처리하는 방식으로 주로 사용하기 때문에 JS와 마크업이 혼합된 형태로 개발이 된다. 따라서 FE 개발자와 UI 개발자가 JSX를 함께 관리하게 되면서 종종 코드 충돌이 생기고, FE 개발자가 개발을 완료한 컴포넌트에 대해서 UI 개발자가 JSX를 수정하는 것은 쉽지 않다.

VAC 패턴은 React 환경에서 UI 개발자와 FE 개발자의 협업 문제를 해결하기 위해 View 컴포넌트에서 JSX 영역을 분리하는 패턴이다.

VAC 패턴은 View 컴포넌트에서 JSX 영역을 Props Object로 추상화하고, JSX를 VAC로 분리해서 개발하는 설계 방법이다. 이런 설계는 비즈니스 로직 뿐만 아니라 UI 기능 같은 View 로직에서도 렌더링 관심사를 분리하는데 목적이 있다.

VAC는 다음과 같은 특징을 가지고 있다.

  • 반복이나 조건부 노출, 스타일 제어와 같은 렌더링과 관련된 처리만을 수행한다.

  • 오직 Props를 통해서만 제어되며 스스로의 상태를 관리하거나 변경하지 않는 stateless 컴포넌트다.

  • 이벤트 함수를 바인딩할 때 어떠한 추가 처리도 하지 않는다.

VAC는 state를 가질 수 없지만 state를 가진 컴포넌트를 자식으로 가지는 것은 가능하다. 이 경우 VAC는 부모 컴포넌트와 자식 컴포넌트 중간에서 개입하지 않고 단순히 props를 전달하는 역할만 한다.

구현 예제

간단한 Counter UI 컴포넌트를 통해 VAC를 만드는 방법을 알아보자.

const Counter = () => {
  const [value, setValue] = useState(0);
  
  return (
    <div>
      <button onClick={() => setValue(value - 1)}>-</button>
      <span>{value}</span>
      <button onClick={() => setValue(value + 1)}>+</button>
    </div>
    );
};

예제는 +, - 버튼을 클릭하면 값이 1씩 증가하거나 감소하는 UI 기능을 포함하고 있다.

Props Object 정의

View 컴포넌트에서 JSX를 추상화한 Props Object를 생성하고 JSX에서 사용할 상태정보나 이벤트 핸들러를 정의한다.

const Counter = () => {
  const [value, setValue] = useState(0);
  
  // JSX를 추상화한 Props Object
  const props = {
    value,
    onDecrease: () => setValue(value - 1),
    onIncrease: () => setValue(value + 1),
  };
  
  // JSX의 유무는 중요하지 않다.
  return <div></div>;
};

JSX를 VAC로 분리

JSX 영역을 분리하여 VAC로 만든다. 이때 View 컴포넌트에 생성한 Props Object 속성을 참고하여 VAC의 Props를 정의한다.
반대로 이미 만들어진 VAC를 View 컴포넌트에 적용할 때는 VAC의 Props를 참고하여 View 컴포넌트의 Props Object 속성을 정의한다.

// VAC
const CounterView = ({ value, onIncrease, onDecrease }) => (
  <div>
    <button onClick={onDecrease}>-</button>
    <span>{value}</span>
    <button onClick={onIncrease}>+</button>
  </div>
);
// View Component
const Counter = () => {
  const [value, setValue] = useState(0);
  
  const props = {
    value,
    onDecrease: () => setValue(value - 1),
    onIncrease: () => setValue(value + 1),
  };
  
  // JSX를 VAC로 교체
  return <CounterView {...props} />;
};

VAC Debugger

만약 VAC가 개발되기 전에 Props Object를 테스트해보고 싶다면 VAC Debugger를 활용할 수 있다.

// VAC Debugger
import VAC from "react-vac";

const Counter = () => {
  const [value, setValue] = useState(0);

  const props = {
    value,
    onDecrease: () => setValue(value - 1),
    onIncrease: () => setValue(value + 1),
  };

  // VAC Debugger로 Props Object 테스트
  return <VAC name="Counter" data={props} />;
};

Props Object를 사용하는 이유

Props Object와 VAC를 사용하지 않고 직접 변수를 선언해 JSX 영역에서 UI 기능의 의존성을 줄이는 것도 가능하다. 하지만 이렇게 개발하는 경우는 UI 기능이 복잡해서 변수나 hook이 많을 때 어떤 것을 JSX에서 사용하는지 한눈에 파악하기 어렵고 디버깅 하기가 번거롭다. 또한, View 컴포넌트 내에서 JSX를 관리하고 있어서 간단한 상태처리의 경우 무의식적으로 JSX에서 바로 적용할 가능성이 있다.

// View Component
const SpinBox = () => {
  const [value, setValue] = useState(0);

  // JSX에서 사용할 값을 미리 선언하여 JSX에 적용
  const onDecrease = () => setValue(value - 1);
  const onIncrease = () => setValue(value + 1);

  return (
    <div>
      <button onClick={onDecrease}>-</button>
      <span>{value}</span>
      <button onClick={onIncrease}>+</button>
    </div>
  );
};

잘못된 VAC 적용 예시

다음과 같이 View 컴포넌트에서 기능이나 상태 제어에 VAC가 관여해서는 안된다.

// View Component
const SpinBox = () => {
  const [value, setValue] = useState(0);

  const props = {
    value,
    step: 1,
    handleClick: (n) => setValue(n),
  };
  
  return <SpinBoxView {...props} />;
};
// VAC
const SpinBoxView = ({ value, step, handleClick }) => (
  <div>
    // VAC에서 value를 제어하는 행위에 관여 
    <button onClick={() => handleClick(value - step)}>-</button>
    <span>{value}</span>
    <button onClick={() => handleClick(value + step)}>+</button>
  </div>
);

올바른 VAC는 핸들러를 이벤트에 바인딩만 할 뿐, 무엇을 하는지에 대해서 관여하지 않는다.

Presentational 컴포넌트와 VAC

VAC 패턴은 Container 컴포넌트에 로직을 위임하고 설계 방식을 따르기 때문에 P&C 컴포넌트 패턴의 한 종류라고 볼 수 있다. 때문에 VAC가 Presentational 컴포넌트와 동일한 역할을 하는 것처럼 혼동되는 경우가 있으나, 두 컴포넌트의 근본적인 차이는 컴포넌트가 View 로직(UI 기능, 상태 관리)을 가질 수 있는지 없는지다.

Presentational 컴포넌트는 View와 관련된 상태를 스스로 제어하는 것을 허용하지만 VAC는 stateless 컴포넌트로 스스로의 상태를 제어하지 않고 항상 부모 컴포넌트에서 Props Object를 통해 관리한다. 따라서 VAC는 Presentational 컴포넌트 보다 더 구체적인 기준을 제시하여 JSX를 처리하는 컴포넌트 관점에서 일관성 있는 설계를 하는데 도움을 준다.

Presentational 컴포넌트

  • 비즈니스 로직과 View의 관심사 분리가 목적

  • Container 컴포넌트에서 비즈니스 로직을 관리하고 Presentational 컴포넌트를 제어

  • Presentational 컴포넌트는 View 로직(UI 기능, 상태 관리)과 렌더링을 담당

VAC

  • View 로직(UI 기능, 상태 관리)과 렌더링(JSX)의 관심사 분리가 목적

  • 비즈니스 로직은 Custom Hook으로 처리

  • View 컴포넌트가 VAC의 Container 컴포넌트 역할을 하며 JSX를 추상화한 Props Object를 관리하여 VAC를 제어

  • VAC는 JSX, Style을 관리하여 렌더링 처리

참고
[React] 디자인패턴 (Presentational-Container, VAC패턴)
React에서 View의 렌더링 관심사 분리를 위한 VAC 패턴 소개

profile
경험은 일어난 무엇이 아니라, 그 일어난 일로 무엇을 하느냐이다.

0개의 댓글