본문 바로가기

Project/프리온보딩

[프리온보딩 프론트엔드 챌린지 1월] VAC 패턴

 

타입스크립트를 배운 지 얼마 되지 않아 프리온보딩 프론트엔드 챌린지에 참여하게 되었다. 챌린지 과제 진행을 위해 여러 정보를 찾아보다가 VAC 패턴에 대해 알게 되었으며, 이 VAC 패턴에 대해 정리해보려 한다.

 

 

 

 

React 관심사 분리

고차 컴포넌트(High Order Component)란? 리액트 공식 문서의 고차 컴포넌트(HOC)에 관한 문서이다. 여기서는 HOC에 대해서는 컴포넌트를 전달받아 새로운 컴포넌트를 반환한다 정도로만 짚고 넘어가면

nicehyun12.tistory.com

이전 게시글 React관심사 분리에서 View와 Model의 분리와 각각의 관심사에 따라 코드를 분리하는 기법인 관심사 분리를 중점으로 정리했었다.

 

[ View와 Model의 분리 ]

 

우선 view와 model의 분리를 먼저 짚고 넘어가면 좋을 것 같다. 내가 패턴을 공부하면서 각각의 패턴에서는 view와 model의 분리한다는 중요한 공통점이 있다는 것을 알게 되었다.

 

view와 model의 분리는 거시적으로 보면 view의 경우 HTML/CSS로 만들어진 결과물이 될 것이고, model의 경우 View에 반영할 수 있는 데이터를 주관하는 영역인 서버가 될 것이다. 

 

view와 model의 분리를 웹 프론트엔드로 조금 더 좁혀 보면 view는 유저와 소통하는 UI, model은 데이터를 UI에 노출하는 로직이 될 것이다. 즉, UI와 비지니스 로직을 분리하는 것이다.

 

 

챌린지 멘토님꼐서 일반적인 프론트엔드 개발자에 대해 아래와 같이 설명하셨다.

  • 서버에서 데이터를 잘 받아서 ➡️ 사용자에게 잘 보여준다.
  • 사용자에게 입력을 잘 받아서 ➡️ 서버로 잘 보내준다.

 

너무나도 명확한 설명이었다. 해당 설명을 나는 아래와 같이 view와 비지니스 로직으로 구분해 보았다.

  • 서버에서 데이터를 잘 받아서 ➡️ 서버에서 데이터를 받는 로직 : 비지니스 로직
  • 사용자에게 잘 보여 준다. ➡️ 화면에 서버에서 넘어온 데이터를 보여주기만 한다. : view
  • 사용자에게 입력을 잘 받아서 ➡️ 화면에 입력된 정보를 비지니스 로직에서 처리할 수 있게 보내주기만 한다. : view 
  • 서버로 잘 보내준다. ➡️  서버에 데이터를 보내는 로직 : 비지니스 로직

 

즉, 비지니스 로직은 view에 대해 관여를 하지 않고 view에서 화면에 데이터를 보여줄 수 있게 보내주기만 하고, view는 비지니스 로직에서 데이터를 어떻게 받아오는지 상관하지 않고 그냥 데이터만 전달받아서 화면에 보여주기만 하는 것이다.

 

 

const Header = () => {
  // 헤더에 필요한 데이터를 관리하는 비지니스 로직

  const isAuth = { // 비지니스 로직을 통해 생성된 객체
    islogin : true
    auth: 0,
  };

  return <HeaderView isAuth={isAuth} />; // prop으로 view로 전달
};

 

리액트에서 고차 컴포넌트를 사용하여 위와 같은 형태로 view와 비지니스 로직의 분리가 가능하다. 즉 Header 컴포넌트는 로그인 관련 로직을 처리해서 HeaderView 컴포넌트에 로그인이 되었는지 안되었는지와 로그인 한 사용자의 등급만 전달받아 UI를 처리하게 될 것이다.

 

이것이 잘 적용된 패턴이 VAC 패턴이다.

 

 

VAC 패턴

 

VAC 패턴의 특징은 아래와 같다.

  • VAC 패턴은 View Asset Component의 약자로 렌더링에 필요한 JSX와 스타일을 관리하는 컴포넌트를 의미한다.
  • VAC 패턴은 View 컴포넌트에서 JSX 영역을 Props Object로 추상화하고, JSX를 VAC로 분리한다.

 

위의 특징을 적용하여 만들어진 VAC(컴포넌트)의 특징은 아래와 같다.

  • 반복이나 조건부 노출, 스타일 제어와 같은 렌더링과 관련된 처리만 수행한다.
  • 오직 props를 통해서만 제어되며 스스로 상태를 관리하거나 변경하지 않는 stateless 컴포넌트이다.
  • 이벤트에 함수를 바인딩할 때 어떠한 추가 처리도 하지 않는다.
❗️비지니스 로직뿐만 아니라 UI 기능 같은 View 로직 또한 분리하여 렌더링 관심사를 분리한다고 한다.

 

위의 특징을 간략하게 살펴보면 아래처럼 받아들일 수 있다.

  • 반복이나 조건부 노출, 스타일 제어와 같은 렌더링과 관련된 처리만 수행한다.
    • ✏️ 앞서 작성한 코드를 예로 들면 HeaderView 컴포넌트는 isLogin이 true이면 로그인 버튼을 false이면 로그아웃 버튼을 화면에 보여주면 된다.
  • 오직 props를 통해서만 제어되며 스스로 상태를 관리하거나 변경하지 않는 stateless 컴포넌트이다.
    • ✏️ 말 그대로 View 컴포넌트에서는 어떠한 상태관리도 하지 않고 그냥 단순하게 props로 전달받아 화면에 보여준다.
  • 이벤트에 함수를 바인딩할 때 어떠한 추가 처리도 하지 않는다.
    • ✏️ 간략하게 함수자체를 전달받아 버튼과 함수를 연결만 시켜준다.

 

위의 특징에서 '이벤트에 함수를 바인딩할 때 어떠한 추가 처리도 하지 않는다'를 조금 더 자세히 살펴보면 아래와 같다.

 

❗️ 잘못된 예시

const Header = () => {
  const [isLogin, setIsLogin] = useState(false);

  const props = {
    islogin: true,
    auth: 0,
    handleLogin: (bool) => setIsLogin(bool),
  };

  return <HeaderView {...props} />;
};
const HeaderView = ({handleLogin}) => {
  return (

    <button onClick={()=>handleLogin(false)} >로그아웃</button>

};
현재 Header 컴포넌트는 인수를 직접 넣어줘야 하는 함수 handleLogin가 담긴 Props Object를 HeaderView 컴포넌트에 전달하고 있으며, HeaderView 컴포넌트에서는 함수에 직접 값을 넣어주고 있다.

 

위의 예시는 '이벤트에 함수를 바인딩할 때 어떠한 추가 처리도 하지 않는다'를 어기고 함수를 바인딩할 때 직접적으로 false를 넣어주고 있다. 정상적인 예시는 아래와 같은 형태이어야 한다.

 

 

✏️ 정상적인 예시

const Header = () => {
  const [isLogin, setIsLogin] = useState(false);

  const props = {
    islogin: true,
    auth: 0,
    handleLogout: () => setIsLogin(false),
  };

  return <HeaderView {...props} />;
};
const HeaderView = ({ handleLogout}) => {
  return <button onClick={handleLogout} >로그아웃</button>
};
Header 컴포넌트에서 Props Object를 전달할 때 setter에 값을 넣어 놓은 상태로 HeaderView 컴포넌트에 전달해 준다. HeaderView 컴포넌트는 Props Object의 함수만 이벤트에 바인딩시켜 준다.

 

 

[타입스크립트와 함께]

 

앞서 언급한 VAC 패턴의 특징 중 'Props Object로 추상화하고' 부분이 타입스크립트와 너무 잘 맞는 부분인 것 같다. 보통 타입스크립트에서 오브젝트를 추상화할 때 인터페이스를 사용한다. 인터페이스를 통해 VAC로 전달하는 Props Object의 타입을 명시해 주게 되면 VAC는 보다 명확하게 '아! 위에서 무슨 일이 일어나는지는 몰라도 내가 이거 이거만 사용하면 되는구나~'라고 받아들일 것이다.

 

 

interface

만약 클래스를 정의했다고 가정해 보겠다. 정의된 클래스를 다른 사용자가 사용할 때 '이 클래스는 어떻게 사용하는 거지..? 내부를 살펴봐야 되네..'와 같은 상황이 발생하게 된다. 이러한 상황은 추상화를 통해 사용자는 '아! 내부에서 무슨 복잡한 일이 일어나는지 모르겠지만, 이거 이거만 사용하면 되는구나!'라고 생각할 수 있게 만들 수 있다.

 

이러한 오브젝트의 추상화를 보다 쉽고, 명확하게 해주는 것이 인터페이스이다.

 

게시글 상단에서 'View는 비즈니스 로직이 어떻게 되는지 상관없고 그냥 나는 데이터만 받아서 보여주면 돼!, 비지니스 로직은 View가 어떻게 돼있든 상관없고 나는 데이터만 전달하면 돼!'라고 글을 작성했었는데 이건 너무 생판 남같이 느껴진다.

 

인터페이스는 계약서와도 같다. 나를 사용하려면 너는 최소한 이거, 이거, 이거는 사용해야한다. 라는 계약서.

 

위처럼 생판 남 같은 상황에 인터페이스를 통해 계약서를 작성하고 계약서에 작성된 규격을 사용한다면, 이전과 똑같이 일하지만 상황은 많이 달라지게 된다. '인터페이스에 이거, 이거, 이거가 있네. 그럼 나는 이거 이거 이거 사용하면 되겠네!' 의 상황이 되는 것이다.

 

❗️때문에 Props Object를 사용할 때 VAC 관점에서, 즉 전달하는 데이터를 사용하는 입장에서 속성명을 지어 전달하는 것이 좋다.

 

 

 

[ Presentational 컴포넌트와 VAC ]

 

const Header = () => {
  // 헤더에 필요한 데이터를 관리하는 비지니스 로직

  const isAuth = { // 비지니스 로직을 통해 생성된 객체
    islogin : true
    auth: 0,
  };

  return <HeaderView isAuth={isAuth} />; // prop으로 view로 전달
};

 

앞서 위와 같은 예시를 들었다. 하지만 해당 예시는 정확하게 말하면 VAC 패턴보다는 Presentational 컴포넌트 패턴일 것이다.

 

VAC 패턴은 비지니스로직과 View를 분리하고, View에서 렌더링을 담당하는 로직과 View를 한 번 더 분리하는 것이기 때문이다.

 

 

Presentational 컴포넌트 패턴에 대해서는 내가 공부하면서 참고했던 블로그에서 자세하게 살펴보면 좋을 것 같다.

 

참고 블로그

 

 

React에서 View의 렌더링 관심사 분리를 위한 VAC 패턴 소개 | WIT블로그

React에서 View의 렌더링 관심사 분리를 위한 VAC 패턴 소개 시작하며 FE개발에서 View는 정보의 시각화 뿐만 아니라 사용자와 상호작용하는 역할을 포함하고 있습니다. 그래서 View를 개발하는 것은

wit.nts-corp.com

 

 

 

프로젝트 적용

 

[ 이전 프로젝트 ]

 

현재 진행 중인 Todo 프로젝트는 배우면서 바로 적용해서 before는 없지만 이전에 작업했던 프로젝트는 아래와 같이 진행했었다.

 

 

우선 해당 컴포넌트는 할인하는 상품을 관리하는 컴포넌트인데 2번에서 서버와 통신하는 비지니스 로직이 포함되어져 있다. 

 

또한 2번 함수의 경우 handleInsertCart, 즉, 상품을 장바구니에 담는 로직이다. 컴포넌트 SaleProduct가 할인하는 상품을 관리를 하는 것인지, 무슨 기능을 담당하는지 한눈에 파악하기가 힘들다.

 

✏️ 대략적으로 봐도 단일 원칙이 전혀 적용되지 않은 것으로 보인다. 

 

또한 함수 handleInsertCart에서 전혀 다른 두 가지 일을 하고 있다. 로그인 여부와 장바구니에 담는 것 생판 다른 행동을 상품을 장바구니에 담는 함수에서 하고있다.

 

✏️ 함수 handleInsertCart가 무엇을 하는 함수인지 추상화가 전혀 되지 않는다.

 

1번의 경우는 View와 View 렌더링 로직, 비지니스 로직을 분리하지 않은 당연한 결과이다. 또한 이미지를 별도로 첨부하지 않았지만 디렉토리가 페이지 기준으로만 구분되어 있어 구분하기가 많이 어렵고, 실제로 프로젝트 진행 당시 해당 로직이 어디에 있는지 찾기가 힘들었었다.

 

 

[ 현재 프로젝트 ]

 

 

현재 프로젝트에서는 우선 디렉토리를 각각의 기능 별로 최대한 구분하려고 했으며, 안 좋은 습관으로 프로그래밍을 하던 나에게는 어색하고 적용하는데 힘이 꽤 들었지만 한 눈에 봐도 해당 컴포넌트가 무엇을 하는지 알 수 있어 좋았다.

 

 

 

또한 VAC 패턴 적용, 즉 View와 View 렌더링 로직, 비지니스 로직의 분리를 위해 별도의 Views 폴더를 만들어 해당 폴더에는  데이터를 전달 받거나, 함수만 전달받아 렌더링 할 수 있게 해주었다.

 

 

View 렌더링 로직, 즉 state를 사용하여 VAC에 내려주는 로직은 해당 컴포넌트에 있는 것이 맞지만, 모달의 특성상 컴포넌트가 상위에 있어야 하기 때문에 View 렌더링 로직을 컴포넌트 AddToDoModal에 작성하지 않았다.

 

 

View 렌더링 로직(모달 렌더링 로직)을 별도의 컴포넌트를 만들어 PropsObject에 담아 컴포넌트 ToDOLayoutView(VAC)에 전달하도록 해주었다. 컴포넌트 ToDOLayoutView(VAC)에서는 당연히 하위 컴포넌트에 props로 받은 것을 전달만 해주고 있다.

 

 

 

느낀점

 

안 좋은 습관으로 프로그래밍하던, 안 좋은 습관인지도 몰랐던 나에게 패턴을 적용해서 로직을 분리하기 위해 디렉토리를 구성하는 것 조차 힘들었다. 구성 된 디렉토리에서 로직을 분리하기 위해 고민하는 시간도 길었기 때문에 엄청 오랜시간이 걸리기도 했다.

 

또한 타입스크립트를 배우고 처음 적용하기 때문에 신경 써야하는 부분이 하나 더 늘었는데 거기에 관심사 분리 등등... 많은 것들이 발목을 잡고 있다. 그리고 내가 하고 있는 것이 맞는 것인지 확신이 들지 않는다.

 

🔥하지만 챌린지, 말 그대로 나에게 하나 하나 도전이다. 그 만큼 조금 더 깔끔해진 코드, 추상 가능한 함수들, 분리된 로직 등등.. 얻는 것이 큰 것같다. 

 

🔥중요한 건 꺾이지 않는 마음, 중꺾만!