View Logic Flow
2024년 1월 사내에 공유한 문서에서 화면을 그리는 코드와 데이터 처리 코드를 나누는 흐름을 제안했습니다. 퍼블리싱과 프론트엔드 작업이 같은 파일에 모이고 응답 가공과 이벤트 처리가 화면마다 다른 위치에 생기는 문제가 있었습니다.
당시 문서는 VAC(View Asset Component)를 참고해 역할과 이름을 맞추려는 기록이었습니다. 모든 팀이 같은 구조를 채택했는지는 확인하지 못해 공통 표준으로 확정했다는 표현은 뺐습니다. 이 글은 그때 정리한 책임의 흐름과 지금 적용할 때 줄일 부분을 설명합니다.
한 파일의 길이보다 변경 이유를 봤습니다
API 응답의 날짜나 상태 코드를 표시 값으로 바꾸는 일, 모달을 열고 요청을 보내는 일, 마크업을 만드는 일이 한 컴포넌트에 들어 있었습니다. 이 상태에서는 화면 디자인을 바꾸는 작업도 요청과 상태 코드를 함께 읽어야 했습니다.
각 변경을 어디에서 처리할지 찾을 수 있게 하는 것이 목표였습니다. 응답 형식이 달라지면 converter를 보고 사용자 행동이 달라지면 화면의 controller를 보도록 정리했습니다.
원문에서 나눈 여섯 역할
| 역할 | 맡길 일 | 넘기지 않을 일 |
|---|---|---|
| Data Fetching | 요청, query key, 로딩과 오류 상태 | 문구와 마크업 |
| Converting | 응답을 ViewModel로 가공 | 화면 이동, 모달 실행 |
| Page Controller | 화면 상태와 사용자 이벤트 | 하위 UI의 세부 마크업 |
| Props Getter | 하위 UI에 필요한 값과 이벤트 정리 | 별도 데이터 원본 만들기 |
| Props Object | View에 전달할 계약 조립 | 같은 값의 중복 보관 |
| View Component | 전달받은 값의 표시와 이벤트 전달 | 요청 정책 결정 |
위 표는 역할을 나눈 것입니다. 여섯 파일을 반드시 만들라는 규칙으로 사용하지 않습니다. 단순 화면에서는 controller 안에서 ViewModel과 props를 함께 조립해도 흐름을 읽는 데 문제가 없습니다.
flowchart LR
Query["요청과 서버 상태"] --> Convert["응답 → ViewModel"]
Convert --> Controller["화면 상태와 이벤트"]
Controller --> Props["View의 props 계약"]
Props --> View["표현 컴포넌트"]
View -->|사용자 이벤트| Controller
Controller -->|변경 요청| Query표시용 값을 다시 state에 복사하지 않습니다
다음은 구조를 설명하기 위해 다시 작성한 예시입니다. 요청 결과를 보여 주기 위해 Effect로 setRows를 실행하는 대신 현재 응답에서 표시 값을 계산합니다. 선택 상태는 사용자 행동으로 바뀌므로 별도 state로 관리합니다.
const useProductPage = () => {
const query = useProducts()
const [selectedId, setSelectedId] = useState<number | null>(null)
const cards = (query.data ?? []).map(toProductCard)
return {
cards,
selectedId,
loading: query.isPending,
onSelect: setSelectedId,
}
}
const ProductPage = () => <ProductView {...useProductPage()} />표시를 위한 파생 데이터는 렌더링 중 계산할 수 있습니다. 외부 시스템과의 동기화가 아닌데 Effect와 state를 추가하면 값이 맞춰지는 시점까지 관리해야 합니다. React의 Effect 사용 기준
계산 비용이 크다면 측정한 뒤 메모이제이션을 검토합니다. converter라는 이름을 붙였다는 이유만으로 캐시하거나 전역 스토어에 넣지는 않습니다.
책임을 나눈 뒤에도 남는 선택
Compound Component를 만들 때는 Context로 props를 전달할 수 있습니다. 다만 Context에 넣었다고 렌더링 비용이 자동으로 줄지는 않습니다. 값이 바뀌는 범위와 구독하는 컴포넌트를 함께 살펴야 합니다.
View가 무엇이든 받는 거대한 props 객체를 요구한다면 책임이 충분히 나뉜 것인지 다시 확인합니다. 반대로 두 줄을 감싼 getter 때문에 파일을 계속 오가야 한다면 그 계층을 합치는 편이 읽기 쉬울 수 있습니다.
실제 UI를 컴포넌트로 뺄지는 다른 질문입니다. 데이터 흐름을 나누는 기준과 DOM 구조를 감싸는 기준을 섞지 않도록 컴포넌트 분리 기준에 따로 정리했습니다.