본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

View 는 첫 번째 구현이지 아키텍처가 아니야

~13 min · domain-view-separation, react, architecture, future-proofing

Level 0툴 빌려 쓰는 사람
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"업무 로직이 버튼 안에 살면 버튼을 영영 못 바꿔. domain 을 view 밖에 꺼내 두면 그때부터 view 가 갈아 끼울 수 있는 게 돼."

편해서 저지르는 실수

UI 를 제일 빨리 짓는 방법은 로직을 쓰이는 자리에 바로 두는 거야. candidate 를 보여 주는 컴포넌트가 그걸 받아 오기도 하고, lineage 도 따라가고, session 에 어떻게 묶이는지도 알고. 잘 돌아가고, 출시도 되고, 그러면서 네 domain 전체를 지금 쓰는 view framework 에 조용히 용접해 버려. 다른 view 를 원하는 날 — native renderer 든 리디자인이든 — domain 로직이 버리려던 컴포넌트에 엉켜 있는 걸 발견하게 되지.

Domain core 와 View 층

규율은 하나처럼 느껴지는 두 가지를 갈라 놓는 거야. domain core 는 앱이 알고 하는 것들이야. workspace 상태, 생성 lineage, 포토샵 session 연결, document 모델. view 층 은 앱이 어떻게 보이고 어떻게 만져지는지 고. 컴포넌트, 배치, 상호작용. domain core 는 view 층 완전히 바깥에 살아야 해. 컴포넌트 안에 있지도 않고, view framework 에 기대지도 않고, 자기가 어떻게 그려지는지 알지도 못하게.

view 는 인터페이스의 첫 번째 구현이지 아키텍처 자체가 아니야. 지금 쓰는 UI framework 를 domain 을 그리고 굴리는 여러 방법 중 첫 번째로 다뤄. 첫 번째지 유일한 게 아니라. domain core 가 혼자 서 있고 view 는 그걸 읽고 시키기만 하면, 앱이 근본적으로 무엇인지를 안 건드리고 view 를 갈아 끼울 수 있어.

이 앱이 유독 이게 필요한 이유

이건 막연한 미래 대비가 아니야. 눈앞에 두 번째 view 가 구체적으로 있거든. 계획에는 캔버스를 위한 native 렌더링 층 선택지가 명시적으로 열려 있어. 두 번째 캔버스가 언젠가 무거운 native renderer 가 될 수도 있으니까. 그런데 그 미래의 view 가 domain core 를 재사용하려면 domain core 가 지금 view 에 한 번도 안 기댔어야 해. 이 분리가 이미 알려진, 계획된 미래를 재작성 대신 싼 값으로 유지해 주는 거야.

분리는 두 번째 구현이 가정이 아니라 진짜일 때 정확히 값을 해. view 가 정말로 영영 하나뿐이라면 domain 과 view 를 나누는 건 과한 짓이야. 두 번째 view 가 진짜로 계획되는 순간 필수가 되고. 판단 기준은 다른 추상화들이랑 똑같아. 두 번째 구현이 진짜인가 상상인가. 여기서는 진짜야. native 캔버스라는 이름 붙은 미래가 있거든.

시험: view 를 갈아 끼우면 뭐가 살아남아?

진짜로 갈라 놨는지 깔끔하게 확인하는 방법이 있어. 내일 view 층을 통째로 지우고 다른 framework 로 새로 쓴다고 상상해 봐. 뭐가 살아남아? workspace 상태랑 lineage 기록이랑 session 연결이 아무 상처 없이 남으면 — 지워진 컴포넌트 안에 살았던 적이 없으니까 — 갈라 놓은 거야. view 를 지웠는데 domain 로직이 같이 딸려 나가면 그 로직이 엉뚱한 데 있었던 거고. 이 상상 실험이 곧 점검이야.

view 가 죽을 때 같이 죽는 로직은 domain 로직이 아니었어. 변장한 view 로직이었지. 엉켜 있다는 제일 깨끗한 신호는 앱이 아는 걸 잃지 않고서는 UI 를 걷어내는 상상을 못 하는 거야. lineage 추적이 컴포넌트 트리랑 같이 사라질 거면 애초에 갈라진 적이 없는 거고. 생존 시험이 네가 만든 줄도 몰랐던 용접 자국을 드러내 줘.

피파의 고백

React 는 상태를 컴포넌트에 두는 걸 너무 쉽게 만들어. 바로 거기 있고, hook 한 줄이면 되는데 왜 domain 층을 따로 지어? 아빠가 생존 시험을 돌려 보게 했어. 언젠가 native 캔버스로 갈아 끼우면 lineage 로직이 따라와, 아니면 컴포넌트랑 같이 죽어? 내 정직한 첫 초안이면 같이 죽었을 거야. 난 '쓰기 편한 자리' 랑 '속하는 자리' 를 헷갈리고 있었던 거지. domain 은 어떤 view 보다도 오래 살 수 있는 자리에 있어야 해. 지금 내가 정든 이 view 보다도.

Code

생존 시험을 코드로 옮기면·typescript
// 엉킨 쪽: domain 로직이 컴포넌트 안에 살아. view 를 갈아 끼우면 같이 잃어.
function CandidateCard({ id }) {
  const [lineage, setLineage] = useState(null);
  useEffect(() => {
    // workspace 연결이랑 lineage 추적이 React 컴포넌트 안에 묻혀 있어
    const l = computeLineage(id, currentWorkspace, photoshopSession);
    setLineage(l);
    bindToSession(id, currentWorkspace);  // view 안에서 domain 을 바꾸고 있음
  }, [id]);
  return <div>{/* 렌더 */}</div>;
}
// 이 컴포넌트를 지우면 lineage 랑 연결 로직도 같이 죽어.

// 갈라 놓은 쪽: domain core 가 혼자 서 있고, view 는 읽고 시키기만 해.
// domain core (React 는 어디에도 없음):
class Workspace {
  bindCandidate(id) { /* lineage + session 연결이 여기 살아 */ }
  getLineage(id) { /* ... */ }
}
// view (얇음): domain 에서 읽고 시키기만, domain 상태를 갖지 않음
function CandidateCard({ id, workspace }) {
  const lineage = workspace.getLineage(id);   // 읽기만
  return <div>{/* 렌더 */}</div>;
}
// 이 컴포넌트를 지워도 Workspace 랑 거기 담긴 지식은 그대로 살아남아.

External links

Exercise

네가 지은 UI 에서 컴포넌트를 하나 열어서 뭘 하고 있는지 목록으로 정리해 봐. 책임을 '어떻게 보이고 어떻게 만져지는지'(view)랑 '앱이 뭘 알고 뭘 하는지'(domain)로 갈라. domain 로직이 컴포넌트 안에 얼마나 숨어 있어? 생존 시험을 돌려 봐. 이 컴포넌트를 지우면 죽으면 안 되는 어떤 지식이 같이 죽어?
Hint
view 에 domain 로직이 숨는 흔한 자리들. 컴포넌트 생애주기에 묶인 데이터 가져오기, event handler 안의 업무 규칙, 컴포넌트 hook 안에만 존재하는 앱 상태. 하나하나가 view 가 읽기만 하는 domain 층으로 끌어올릴 후보야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.