요즘 팀원과 효율적으로 협업하는 방법에 대해 관심이 많아졌다.
지난 몇 달간의 경험이 협업에 대한 마인드 셋을 완전히 뒤바꾸어 놓았기 때문이다.
협업시 팀원과의 Conflict을 최소화 하려면 어떤 방법을 사용해야 하는지, 더욱 가독성 높고 Clean한 코드를 작성하려면 어떤 방법을 따라야 하는 지 등등,,
효과적인 협업 방식을 서칭하다 발견한 녀석이 있는데 바로 오늘 포스팅 할 VAC Pattern 이라는 방법론이다.
팀원들과의 협업시 많은 이점을 가져다줄 것으로 보여져 바로 내 프로젝트에 적용해보기로 결정했다!
먼저 VAC Pattern이 뭐하는 친구인지 알아보자.
VAC Pattern은 View Asset Component의 약자로 효과적으로 JSX와 Style를 관리하여 UI와 비즈니스 로직을 분리하는데 목적을 둔 컴포넌트 설계 방법론
조금 더 풀어서 설명하자면
복잡한 UI (User Interface)를 가진 소프트웨어에서 UI 요소와 비즈니스 로직을 분리하고, 데이터 요소를 처리하는 컴포넌트와 UI 요소를 처리하는 컴포넌트를 분리하여 개발하는 패턴이다.
즉, 렌더링에 필요한 JSX 컴포넌트와 스타일을 관리하는 컴포넌트로 나눈다는 것이다.
이미지를 잘 살펴보면 일반적인 패턴과는 다르게 View 컴포넌트에서 JSX 영역을 Props Object로 추상화하고, JSX를 VAC로 분리해 놓은 것을 확인할 수 있다.
오직 props를 통해서만 제어되며 스스로의 상태를 관리하거나 변경하지 않는 stateless 컴포넌트의 형태를 가진다.
이 말은 팀원과 어떤 데이터를 props로 넘겨주고 어떻게 처리할 건지 미리 상의해 놓으면 완전히 독립적으로 작업이 가능하다는 것이다.
또한 완전히 다른 컴포넌트에서 작업하기 때문에 Conflict이 최소로 줄어든다는 점이다.
내 프로젝트에 적용 전 간단한 예시를 한 번 보자.
const CountBox = () => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count - 1)}>-</button>
<span>{value}</span>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
};
위 코드는 +, – 버튼을 클릭하면 값이 1씩 증가하거나 감소하는 예제다.
이 예제를 VAC Pattern으로 바꾸려면 먼저 VAC와 View Component로 분리해야 한다.
View 컴포넌트 먼저 만들어보자.
//View Component
const CountBox = () => {
const [count, setCount] = useState(0);
// JSX를 추상화한 Props Object
const props = {
count,
onDecrease: () => setCount(count - 1),
onIncrease: () => setCount(count + 1),
};
return (
<></> //현재는 어떤 JSX를 리턴하는지는 상관없다
);
};
View 컴포넌트에서 JSX를 추상화한 Props Object를 생성하고 JSX에서 사용할 상태정보나 이벤트 핸들러를 정의한다.
초기 코드에서 사용되었던 count, setCount 함수를 props 객체에 정의했다.
이 props 객체는 이제 VAC에서 바인딩만 해주면 된다.
그럼 이제 VAC를 만들어보자.
//VAC
const VCountBox = ({count, onIncrease, onDecrease}) => {
return (
<div>
<button onClick={onDecrease}>-</button>
<span>{count}</span>
<button onClick={onIncrease}>+</button>
</div>
);
};
VAC에서는 우선 View 컴포넌트에 생성한 Props Object 속성을 참고하여 VAC의 Props를 위와 같이 정의해준다.
그리고 View 컴포넌트로부터 받은 props를 바인딩해주면 끝이다.
컴포넌트 명은 다양한 방식이 있지만 대체적으로 VAC라는 것을 쉽게 알기 위해 네이밍 앞에 V를 붙이는 게 일반적이다.
다시 View 컴포넌트로 이동해 방금 만든 VAC를 리턴해주면 완성이다.
//View Component
const CountBox = () => {
const [count, setCount] = useState(0);
// JSX를 추상화한 Props Object
const props = {
count,
onDecrease: () => setCount(count - 1),
onIncrease: () => setCount(count + 1),
};
return (
<VCountBox {...props}/>
);
};
아래의 코드를 살펴보자.
//View Component
const CountBox = () => {
const [count, setCount] = useState(0);
// JSX를 추상화한 Props Object
const props = {
count,
step : 1,
handleClick: (n) => setCount(n),
};
return (
<VCountBox {...props}/>
);
};
//VAC
const VCountBox = ({count, onIncrease, onDecrease}) => {
return (
<div>
<button onClick={() => handleClick(count - step)}>-</button>
<span>{count}</span>
<button onClick={() => handleClick(count + step)}>+</button>
</div>
);
};
위 코드를 살펴보면 View 컴포넌트에서 생성된 handleClick 함수가 VAC에서 count와 step을 인자로 받으면서 View 컴포넌트의 기능이나 상태 제어에 VAC가 관여하고 있다.
올바른 VAC는 핸들러를 이벤트에 바인딩만 할 뿐, 무엇을 하는지에 대해서 관여하지 않는다.
CountBox 기능이 0 ~ 10 범위만 사용하도록 증가 감소 버튼의 disabled 상태를 처리한다고 가정해보자.
const CountBox = () => {
const [count, setCount] = useState(0);
return (
<div>
<button disabled={count < 1} onClick={() => setCount(count - 1)}>
-
</button>
<span>{count}</span>
<button disabled={count > 9} onClick={() => setCount(count + 1)}>
+
</button>
</div>
);
};
JSX에 직접 disabled 조건을 처리한다.
VAC Pattern
// View Component
const CountBox = () => {
const [count, setCount] = useState(0);
const props = {
count,
disabledDecrease: count < 1,
disabledIncrease: count > 9,
onDecrease: () => setCount(count - 1),
onIncrease: () => setCount(count + 1),
};
// JSX를 VAC로 교체
return <VCountBox {...props} />;
};
// VAC
const VCountBox = ({ count, disabledDecrease, disabledIncrease, onIncrease, onDecrease }) => (
<div>
<button disabled={disabledDecrease} onClick={onDecrease}>
-
</button>
<span>{count}</span>
<button disabled={disabledIncrease} onClick={onIncrease}>
+
</button>
</div>
);
VAC Pattern에서는 View Component에서 Props Object를 정의하고 VAC에서 상태를 바인딩하기 때문에 렌더링에 더 직관적인 형태로 상태를 관리해야한다.
예를 들어 isMax, isMin 보다는 disabledDecrease, disabledIncrease가 렌더링에서 어떤 역할을 하는지 유추하기 더 쉽기 때문이다.
View 컴포넌트에서는 JSX에 어떻게 상태가 적용되는지 신경 쓸 필요가 없으며, VAC(JSX) 관점에서는 어떤 조건에서 버튼이 활성/비활성 되는지를 파악할 필요가 없다.
초기 코드
const SecondPage = ({ selectedInfo, selectedValues, postSelected }: IProps) => {
const hasValue = (key: number) => {
return selectedValues[key] !== 0;
};
const disabledSubmit = selectedValues.includes(0);
return (
<StSecondPage>
<StBody>
{SECOND_QUESTION.map(({ id, state, title, button }) => (
<div key={id}>{hasValue(id - 1) && <Question state={state} title={title} button={button} />}</div>
))}
</StBody>
<StFooter>
<StButton
onClick={() => {
postSelected(selectedInfo);
}}
disabled={disabledSubmit}>
결과 보기
</StButton>
</StFooter>
</StSecondPage>
);
};
위 코드는 VAC Pattern 적용 전 코드이다.
코드를 살펴보면 boolean을 반환하는 hasValue 라는 함수와 disabledSubmit을 확인할 수 있고 유저의 답을 서버로 보내는 postSelected 함수가 있다.
순차적 리스팅을 위해 유저의 답을 배열로 저장하며( 초기값[ 0, 0, 0, 0 ] ) 질문지의 답을 선택 시 0을 선택한 값으로 바꿔주었다.
앞의 질문에 대한 답을 선택했는지 확인하기 위해 data[ id - 1 ]의 형식으로 배열 요소에 접근 후 0인지 아닌지 boolean을 반환하는 함수를 질문지가 렌더링 되는 map 앞에 논리연산자를 추가하여 구현한 코드이다.
우리는 이 함수들을 View component에서 작성하고 VAC에서 바인딩만 해주면된다.
만들어보자.
//View Component
const SecondPage = ({ selectedValues, postSelected }: IProps) => {
const hasValue = (id: number) => selectedValues[key] !== 0;
const submit = () => !selectedValues.includes(0) && postSelected();
const disabledSubmit = selectedValues.includes(0);
const pageProps: ISecondPageProps = {
hasValue,
submit,
disabledSubmit,
};
return <VSecondPage {...pageProps} />;
};
export default SecondPage;
//VAC
const VSecondPage = ({ submit, questions, disabledSubmit }: ISecondPageProps) => {
return (
<StSecondPage>
<StBody>
{questions.map(({ id, state, title, button }) => (
<div key={id}>{hasValue(id - 1) && <Question state={state} title={title} button={button} />}</div>
))}
</StBody>
<StFooter>
<StButton onClick={submit} disabled={disabledSubmit}>
결과 보기
</StButton>
</StFooter>
</StSecondPage>
);
};
다음과 같이 VAC Pattern으로 만들어 보았다.
하지만 위의 방식은 잘못된 VAC Pattern이다.
hasValue 함수가 map으로 렌더링되고 있는 각각의 질문지 id 값을 인자로 받아버리면서 규칙을 위배했기 때문이다.
앞서 설명했듯 올바른 VAC는 핸들러를 이벤트에 바인딩만 할 뿐, 무엇을 하는지에 대해서 관여하지 않는다.
자 다시 수정해보자.
//View Component
const SecondPage = ({ selectedValues, postSelected }: IProps) => {
const questionIdx = useRecoilValue(questionIdxState);
const submit = () => !selectedValues.includes(0) && postSelected();
const questions = QUESTIONS.slice(4, questionIdx);
const disabledSubmit = !selectedValues.includes(0);
const pageProps: ISecondPageProps = {
submit,
questions,
disabledSubmit,
};
return <VSecondPage {...pageProps} />;
};
const VSecondPage = ({ submit, questions, disabledSubmit }: ISecondPageProps) => {
return (
<StSecondPage>
<StBody>
{questions.map(({ id, state, title, button }) => (
<div key={id}>{<Question state={state} title={title} button={button} />}</div>
))}
</StBody>
<StFooter>
<StButton onClick={submit} disabled={disabledSubmit}>
결과 보기
</StButton>
</StFooter>
</StSecondPage>
);
};
무엇을 하는지에 대해서 관여해서는 안 되기 때문에 질문지의 idx를 관리하는 questionIdx를 추가로 생성하고 유저가 질문에 대한 답을 했을 시 카운팅하는 이벤트를 질문지 답안 버튼에 바인딩한다.
질문지들을 하나의 배열로 관리하고 slice 메소드를 통해 idx를 기준으로 자르는 방식으로 구현했다.
결과적으로 View 컴포넌트의 기능이나 상태 제어에 VAC가 관여하지 않게 되어 올바른 VAC 핸들러가 완성되었다!
VAC Pattern을 프로젝트에 적용하면서 몇 가지 느낀 점을 적으려 한다.
먼저 VAC Pattern은 Typescript와 매우 잘 맞는다는 걸 느꼈다.
Props가 가져야 할 값을 interface로 구현하고 이를 통해 View에 필요한 props들을 좀 더 직관적으로 확인할 수 있었다.
또 VAC는 View 컴포넌트로부터 내려온 props interface만 보고 컴포넌트를 설계하고 스타일 할 수 있으며 어떻게 props가 내려오는지는 신경쓸 필요가 없었다.
View와 Asset을 분리하여 각각 독립적으로 개발하고 유지보수할 수 있도록 하여, 서로의 변경에 대한 영향을 최소화할 수 있기 때문에 팀원과의 협업시 Conflict 확률이 매우 낮아질 것으로 예상된다.
같은 파일을 여러명이서 수정할 경우 나타날 Conflict은 정말 상상만해도 끔찍하지만 VAC Pattern을 적용하면 그런 걱정은 하지 않아도 된다.
하지만,,, 당연히 장점만 있는 것은 아니었다.
먼저 관리해야 할 파일이 최소 1.5배에서 2배까지 늘어난다.
원래는 하나의 파일이 VAC Pattern을 적용하면 View Component와 VAC 두 개의 파일로 나누어지기 때문이다.
만약 프로젝트의 규모가 크다면,, 정말 많은 파일이 추가적으로 생겨날 거다.
그래서 일반적으로 VAC Pattern은 페이지 단위로 적용되고 짜잘한 컴포넌트나 비중이 적은 파일들은 그냥 일반적으로 형식으로 사용된다고 하니 팀원과 협의하에 정하면 될 것 같다.
두 번째는 당연하게도 코드의 양이 늘어난다.
단순히 VAC와 View Component로 파일을 분리하면서 생겨나는 물리적인 양이 아니라, 간단하게 JSX 영역에서 처리될 수 있는 코드를 View Componenet에서 관리하게 되면서 추가적인 코드를 작성해야 할 경우가 생겨난다는 거다.
나의 경우에도 그냥 매핑되고 있는 요소의 id 값을 받아 렌더링하면 됐었지만 VAC Pattern을 적용하면서 추가적으로 state를 생성해야 했다.
VAC Pattern에 대한 이해도가 어느 정도 있다면 충분히 도입 해볼만한 방법론인 것 같다!