몇 주 전에 네이버 D2에서 VAC 패턴에 관한 소개 영상을 보고, 괜찮아 보여서 정리해서 올린다. 사실 전에 했었던 Presentational and Container 패턴은 이걸 위한 밑밥이었다!
저번에 소개했던 Presentational and Container 패턴은 로직과 뷰의 관심사를 분리하는 데 목적이 있는 패턴이다. VAC 패턴은 Presentational and Container 패턴과 비슷하지만, 스타일과 로직을 더욱 적극적으로 분리하는 패턴이다.
위 다이어그램은 VAC 패턴을 설명하는 거의 대부분의 아티클에 다 튀어나오는 것 같은데, VAC 패턴을 잘 설명하고 있기 때문에 그런 것 같아서 나도 써먹어 볼 것이다. VAC를 표현하는 데 필요한 모든 내용을 props 객체로 만들어, 그대로 VAC 컴포넌트에게 내려주는 것이 중요한 점이다.
VAC는 View Asset Component
의 약자이다. 따라서 VAC 컴포넌트라는 말은 역전앞같은 겹말이긴 한데, 이건 논문이 아니라 그냥 혼자 보려고 쓰는 글이므로 불편하면 자세를 고쳐앉으면 되겠다. 최근에 허먼밀러 에어론을 샀는데 아주 자세가 편하므로 구입을 고려해보자.
VAC 컴포넌트는 Presentational 컴포넌트와 같이 stateless한 컴포넌트이다. 즉 스스로 상태나 외부로부터 데이터를 받아오는 등의 비즈니스 로직을 갖지 않는다. 더 나아가, Presentational 컴포넌트는 간단한 연산 수행 정도는 했던 것과 달리, 이제는 간단한 연산 수행조차도 하지 않고 props로 받아온 내용을 그대로 하위 컴포넌트에 전달만 하게 된다. 예시는 지난 번 Article.tsx
을 재탕했다.
const DEFAULT_TITLE = 'NO_TITLE'
const DEFAULT_AUTHOR = 'ANONYMOUS'
const DEFAULT_DESCRIPTION = 'NO_DESCRIPTION'
// Presentational 컴포넌트
const Article = ({ article }: { article: Article }) => {
const title = article.title || DEFAULT_TITLE
const author = article.author || DEFAULT_AUTHOR
const description = article.description || DEFAULT_DESCRIPTION
return (
<li>
<div>
{`title: ${title}`}
</div>
<div>
{`author: ${author}`}
</div>
<div>
{`description: ${description}`}
</div>
</li>
)
}
// VAC 컴포넌트
const Article = ({ title, author, description }) => (
<li>
<div>
{`title: ${title}`}
</div>
<div>
{`author: ${author}`}
</div>
<div>
{`description: ${description}`}
</div>
</li>
)
title
, author
, description
에 대한 기본값 계산 정도는 했던 Presentational 컴포넌트와는 달리, VAC 컴포넌트는 그 기본값 계산조차도 상위 stateful 컴포넌트에게 맡긴다. VAC 컴포넌트가 하는 것은 props로 받아온 내용들을 그냥 JSX에 뿌려주는 것 뿐이다.
NewsList.tsx
의 예시 역시 훌륭한 VAC 컴포넌트이다. articles
와 onRefreshClick
을 받아와서 뿌려주기만 하고 있기 때문이다.
type Props {
articles: Article[]
onRefreshClick: () => void
}
const NewsList: FC<Props> = ({ articles, onRefreshClick }) => (
<div>
<ul>
{articles?.map((article) => (
<Article article={article} />
)}
</ul>
<div onClick={onRefreshClick}>
Refresh!
</div>
</div>
)
그런데 만약 articles
의 길이가 10 이상이면 새로고침 버튼을 노출하지 않는 스펙이 추가되었다고 하자. 아래와 같이 그러한 스펙을 맞춰줄 수 있을 것이다.
type Props {
articles: Article[]
onRefreshClick: () => void
}
const NewsList: FC<Props> = ({ articles, onRefreshClick }) => (
<div>
<ul>
{articles?.map((article) => (
<Article article={article} />
)}
</ul>
{articles?.length < 10 && (
<div onClick={onRefreshClick}>
Refresh!
</div>
)}
</div>
)
하지만 articles?.length < 10
을 계산하는 것 조차 용납될 수 없다! VAC 컴포넌트는 디자인의 영역이지, 로직의 영역이 아니기 때문이다! 이렇게 보여주는 조건을 새로 다는 것은 FE 개발자가 처음에는 하긴 해야하겠지만, 그 다음부터 보여주지 않는 스펙이 15개로 바뀌기라도 한다면 FE개발자가 디자인의 영역인 VAC 컴포넌트를 또 건드려야 하기 때문이다.
따라서 처음부터 showRefresh
를 props로 넘겨주고, refresh 버튼을 보여주는 것은 해당 조건에만 의존하도록 구현하자.
type Props {
articles: Article[]
onRefreshClick: () => void
showRefresh: boolean
}
const NewsList: FC<Props> = ({ articles, onRefreshClick, showRefresh }) => (
<div>
<ul>
{articles?.map((article) => (
<Article article={article} />
)}
</ul>
{showRefresh && (
<div onClick={onRefreshClick}>
Refresh!
</div>
)}
</div>
)
이제 showRefresh
를 결정하는 것은 NewsList
컴포넌트를 사용하는 stateful한 컴포넌트의 몫이다.
// NewsSection
const NewsSection: FC = () => {
const [articles, setArticles] = useState<Article[]>(null)
const fetchArticles = async () => {
const { data } = await axios.get(GET_ARTICLES_URL)
setArticles(data.articles)
}
const showRefresh = articles?.length < 10
// props 객체를 만들어, 그대로 VAC 컴포넌트에게 넘겨준다.
const props = {
articles,
onRefreshClick: fetchArticles,
showRefresh
}
useEffect(() => fetchArticles(), [])
return <NewsList {...props} />
}
이제 디자인의 영역과 로직의 영역이 더욱 극명하게 나뉘게 되었다! 마크업 개발자의 수정내용과 비즈니스 로직에 대한 수정 내용이 이제는 겹치지 않는다.