VAC 패턴으로 비즈니스 로직을 분리해보자 (+ 프로젝트에서 제대로 적용해보기)
😥문제 인식
새로운 팀프로젝트를 하면서 디자인패턴을 새롭게 적용해보고 싶었다.
이유는 그동안 프로젝트를 하면서 하나의 컴포넌트에 뒤섞인 UI + 복잡한 비즈니스 로직들로 가독성이 너무 떨어졌고, 유지보수 하기도 힘들었다.
그런 단점을 보완해줄 패턴으로 MVC 패턴을 접했었는데 고민이 꽤 해소되었던 경험이 있어 이와 유사한 리액트에서 적용 가능한 패턴을 찾아보았다.
처음에는 Compound Component 패턴을 알아봤는데 여기서 스타일 컴포넌트를 따로 분리해서 저장한 것이 가독성 향상에 도움될 것 같아 이 점은 나도 적용하기로 했다.
다만 VAC패턴을 접하고 내가 원한 로직 분리를 위한 디자인 패턴이어서 이것에 대해 알아보기로 했다!
VAC 패턴이란?
VAC 패턴은 View Asset Component의 준말로, JSX와 Style를 관리하여 UI와 비즈니스 로직을 분리하는데 목적을 둔 컴포넌트 설계 방법론이다.
해당 사진과 같이, 기존에는 UI와 비즈니스 로직에 대한 상태(state)를 props와 다양한 컴포넌트에서 가졌다면, VAC 패턴은 Business Logic 컴포넌트에서 props에게 모든 상태를 전달하도록 위임하여 로직을 분리하는 방법이다.
VAC에서는 값을 모두 props에서 받아서 쓰고 렌더링만 수행한다.
따라서 VAC의 특징은 다음과 같다!
- 오직 렌더링에 관련된 처리만 수행한다.(반복, 조건부 렌더링 등)
- 오직 props를 통해서만 제어되며 스스로의 상태를 관리하거나 변경하지 않는 stateless 컴포넌트이다.
- 이벤트에 함수를 바인딩할 때 인자 전달 등 어떠한 추가 처리를 하지 않는다.
- VAC는 state를 가질 수 없지만 state를 가진 컴포넌트를 자식으로 가지는 것은 가능하다. 이 경우 VAC는 부모 컴포넌트와 자식 컴포넌트 중간에서 개입하지 않고 단순히 props를 전달하는 역할만 한다.
VAC 잘못된 예시
export const Item(){
const [count, setCount] = useState(1);
return (
<ItemView count = {count} setCount = {setCount} />;
)
}
export const ItemView(props){
return (
<div>
<h1>아이템1</h1>
<span>수량 {props.count}개</span>
<button onClick={()=>{props.setCount(count+1)}}>추가하기</button>
</div>
)
}
이렇게 VAC에서 직접 state를 변경하면 안된다. VAC는 state에 대해 몰라야 한다. 그럼 이것을 어떻게 올바르게 바꿀 수 있을까?
VAC 올바른 예시
export const Item(){
const [count, setCount] = useState(1);
//값을 변경하는 비즈니스 컴포넌트에서 함수를 생성
const increaseCount = () => {
setCount(count+1);
}
return (
<ItemView count = {count} increaseCount = {increaseCount} />;
)
}
export const ItemView(props){
return (
<div>
<h1>아이템1</h1>
<span>수량 {props.count}개</span>
// VAC에서는 이벤트 바인딩만 처리
<button onClick={()=>{props.increaseCount}}>추가하기</button>
</div>
)
}
처음에 이 패턴을 적용하기로 정하고 나름대로 VAC로 분리하긴 했는데 프로젝트 마감이 너무 촉박해서 제대로 적용되지 않은 부분이 많았다. 그래서 리팩토링을 통해 제대로 적용해보기로 했다.
🔥VAC 패턴 제대로 적용해서 리팩토링 하기
첫번째 리팩토링 : 하나의 VAC에 모든 props 보내기 -> VAC 안에 state 가진 컴포넌트 포함해서 여러 계층으로 분리하기
먼저 내 상황을 간단하게 설명하자면, 나는 떡볶이 큐레이팅 서비스를 만들었는데 PickTypes 라는 컴포넌트는 떡볶이 선택 옵션이 나열되어 있다. 개발할 때 나름 VAC를 분리하려고 했지만 실패해서 문제점이 몇가지가 있다.
난 VAC 안에 state를 가진 컴포넌트를 자식으로 가지는 것은 가능하다는 점을 모르고 하나의 VAC에만 렌더링을 수행해야 하는줄 알아서 필요한 모든 비즈니스 로직을 PickTypes.tsx에 썼는데 PickTypesView안에 자식으로 갖게 하는 걸로 수정했다.
그리고 모든 props를 제일 상위 PickTypes에서 넘겨줄 필요는 없으니 props drilling을 방지하기 위해서 해당 state가 필요한 하위 컴포넌트에서 해당 값들을 선언하기로 했다.
VAC 패턴이랍시고 PickTypesView.tsx로 만들어놓고 비즈니스 로직이 포함된 것을 볼 수 있다. 급하게 로직을 추가하느라고 그렇게 되었다..
VAC는 값의 상태에 대해 관여하지 않는다. 즉 핸들러에 인자값을 넘겨주거나 상태에 대해 관여하면 안된다.
조건부렌더링의 경우에도 미리 조건을 판단하여 boolean값을 넘겨줘야 한다.
이제 이 문제점들을 고쳐보기로 했다.
1) Business Logic에 대해 함수로 분리
answerList는 이렇게 배열안에 모든 answer가 담겨있는 형태이고, 각 객체의 code 값 ch뒤에 오는 첫번째 숫자가 질문에 맞는 값이다.
여기서 각 질문별 answer들을 묶어줘야 하고, 선택 안함에 해당하는 '없음' 값에 해당하는 객체들도 걸러줘야 한다.
그래서 걸러내는 함수를 만들어줬다.
새롭게 걸러줘서 2차원 배열을 만들었다.
이렇게 하면 질문 배열의 index와 답변 배열의 index가 같게될 것이다.
이렇게 거른 answerList를 넘겨줘서 하위 컴포넌트에서 따로 조건을 판단(비즈니스 로직)할 필요가 없게 만들어줬다.
그리고 해당 함수의 리턴값을 새로운 배열로 하고, VAC에 넘길 때 해당 함수를 호출한 리턴값을 보내주기로 했다.
2) 조건부 렌더링에 대한 처리
(PickTypesListView.tsx)
리팩토링 중이라 에러가 많다.
여기는 VAC이기 때문에 비즈니스 로직을 분리해줘야 한다.
내가 만든 UI는 조건부 렌더링이다. 1번 질문에서 '다른거' 를 선택했을 때만 2번 질문이 보이고 5번 질문에서 '매운거'를 선택했을 때만 6번 질문이 보인다.
이것에 대한 조건문을 따지는 비즈니스 로직을 계산해줘야 하는데 어떻게 할 수 있을까?
찾아보니 조건부 렌더링의 경우 boolean 값을 받아 view에 나타내야 한다고 하여 비즈니스 로직에서 boolean을 return할 함수를 만들어줬다.
(PickTypesList.tsx) - 비즈니스 로직 컴포넌트
(PickTypesListView.tsx) - VAC
무슨 로직이냐면, 넘겨받은 questionIndex(질문 번호)에 대한 선택이 '다른거' 이거나 '매운거' 라면 true를 반환하는 것이다. 리턴값이 true면 숨겨둔 질문을 보여준다.
비즈니스 로직이 분리되었지만 이건 view에서 해당 함수를 호출하면서 questionIndex 인자를 넘겨줘야 하는거라 View 컴포넌트의 기능이나 상태 제어에 VAC가 관여해서는 안된다는 원칙에 위배되는 것 같다.
그래서 방법을 좀더 생각해보기로 했다.
더 좋은 방법이 없을까?🤔
PickTypesList.tsx 에서 해당 함수를 정의하는 게 아니라, map을 돌며 각각의 question이 렌더링되는 자식 컴포넌트를 만들어서 해당 함수를 정의하면 따로 questionIndex를 넘길 필요가 없을것이다!
(EachList.tsx) - 비즈니스 로직 컴포넌트
(EachListView.tsx) - VAC
이렇게 props로 상위 컴포넌트에서 questionIndex를 전달받은 index를 통해 판별하여 리턴값을 반환한다면 VAC에서는 인자를 넣어주지 않고 완전히 로직 분리가 가능하다!
조금만 더 생각해보면 되는거였다ㅎㅎ
3) 그밖의 로직 분리
다른 컴포넌트들도 이런식으로 모두 분리해줬다.
또한 하나의 VAC에 모든 props 보내기 -> VAC 안에 state 가진 컴포넌트 포함해서 여러 계층으로 분리하기로 수정해줬다.
그러다보니 view 컴포넌트가 많이 생겨서 view만 따로 모아놓기로 했다.
PickTypes 라는 하나의 페이지에 해당하는 전체 파일구조는 이렇다.
👍장점
사용해보니 비즈니스 로직이 분리되어 가독성 향상에 분명 도움이 되었다. 또 스타일 코드도 분리한 것은 정말 잘한 일이다.
또한 원칙이 명료하고 단순하여 도입하는데 어렵지 않다.
그리고 원래 난 비즈니스 로직에 대해서 함수화를 하지않고 줄줄이 쓸 때도 꽤 있었는데, VAC 패턴을 사용함으로써 비즈니스 로직을 넘겨주어야 뷰로직에서 사용할 수 있기 때문에 모든 비즈니스 로직을 반드시 함수로 만들어 props로 넘겨줘야 했다. 함수에 대한 네이밍, 함수 쪼개서 하나의 기능만 동작하게 하기 등에 대해 자연스럽게 신경쓰게 되어 더욱 유지보수하기 좋은 코드를 만들 수 있었다. 정말 큰 장점!
😓 단점
단점이 있다면 props가 당연히 많아진다는 것이다. 하나의 비즈니스 컴포넌트는 적게는 5개에서 많게는 10개 가까이 props를 넘겨줘야 했다.
그리고 두배의 파일이 생긴다는 점도 하나의 단점이다.
state변경 함수같이 사소한 것도 모두 분리해줘야 한다는 점도 조금 번거롭다.
하지만 이런 것들을 모두 고려해봐도 장점이 더 많은 패턴이라는 생각이 들어 한번쯤 써보길 추천하고 싶다.
참고로 리팩토링은 힘들다^^ 첨부터 신경써서 코드를 작성하는 게 중요한 것 같다!