MVC, Flux, Redux를 공부하고 나서 이런 질문이 남았다. 「MVVC인가? 그런 것도 있지 않아?」 이름을 정확히 기억하지 못했는데, 말하려던 건 MVVM(Model-View-ViewModel)이었다. 끝 글자가 C가 아니라 M이다.
MVVM은 이미 한 번 지나간 이름이었다. FSD 글에서 프론트엔드 구조 후보를 고를 때 「안 A: MVC / MVVM」으로 나왔고, 깊이 비교하지 않고 넘겼다. 이 글은 그걸 채운다.
이 글은 내가 AI 대화 상대인 Claude(Anthropic이 만든 AI)와 대화하며 공부한 것을 옮긴 것이다.
먼저: 이 글의 근거는 Microsoft 문서와 Vue 문서이고, MVVM을 누가 처음 만들었는지는 확인하지 못했다
MVVM을 설명한 문서 둘을 원본으로 받아 읽었다.
- Microsoft의 .NET 문서. GitHub(코드와 문서를 올려 두는 사이트)에 있는 저장소 dotnet/docs의
docs/architecture/maui/mvvm.md(커밋9a4e8b4판)다. MVVM의 세 부품과 그 관계를 가장 자세히 적은 문서였다. 이 글에 옮긴 MVVM 문서 인용은 모두 여기서 왔다 - Vue 2 공식 문서. 저장소 vuejs/v2.vuejs.org의
src/v2/guide/instance.md다. Vue는 화면을 만드는 자바스크립트 도구로, React와 같은 일을 한다. 문서에 이런 문장이 있다
Although not strictly associated with the MVVM pattern, Vue’s design was partly inspired by it.
엄밀히 MVVM 패턴에 딸린 것은 아니지만, Vue의 설계는 일부 MVVM에서 영감을 받았다.
「MVVM을 처음 누가 만들었나」는 두 문서 어디에도 없었다. 백과사전 같은 다른 출처는 이 환경에서 접속할 수 없었다. 그래서 이 글에서는 그 이야기를 하지 않는다.
먼저: 이 글의 예제는 우리 앱의 좋아요 버튼이다
앞의 세 글과 같은 예제를 쓴다. 우리 client 앱은 방문자가 보는 회사 공개 사이트로, Next.js로 만든 React 앱이다. 후기 상세 화면에 하트 모양 좋아요 버튼과 좋아요 수가 있다. 하트를 누르면 화면이 서버(좋아요 수를 세는 우리 자바 백엔드)에 요청을 보내고, 서버가 돌려준 새 수로 화면을 다시 그린다. 우리 앱은 MVVM 방식이 아니다. MVVM으로 짜려면 데이터 바인딩(뒤에서 본다)을 해 주는 화면 도구가 필요한데, Vue나 .NET MAUI(.NET은 Microsoft가 만든 프로그램 개발 바탕으로 자바와 비슷한 C# 언어를 쓰고, MAUI는 그 위에서 휴대폰·PC 화면 앱을 만드는 도구다) 같은 도구가 그 일을 한다.
React에서 상태는 컴포넌트가 기억하고 있다가 바뀌면 화면을 다시 그리게 만드는 값이다. 좋아요 수나 「눌렀는지」 같은 값이다. React도 상태가 바뀌면 화면을 저절로 다시 그린다. 그래서 「React도 MVVM 아닌가?」 싶을 수 있는데, 다른 점이 있다. React는 한 방향이다. 값이 바뀌면 화면이 바뀌지만, 사용자가 입력 칸에 글자를 쳐도 그 글자가 값에 저절로 들어가지 않는다. 입력이 바뀔 때마다 「이 글자로 값을 바꿔라」는 코드를 직접 써야 한다. 뒤에서 볼 MVVM의 양방향 바인딩은 그 반대 방향까지 저절로 잇는다.
우리 React 앱은 MVVM 같은 바인딩 없이, 좋아요 수를 적어 두는 작은 보관함과 「바뀌면 알려 주세요」 명단을 직접 만들어 쓴다. 하트를 누르면 코드가 보관함 값을 직접 고치고 명단에 알려서 화면을 다시 그린다. React가 저절로 다시 그리는 건 React가 관리하는 상태가 바뀔 때뿐이다. 이 보관함은 같은 버튼 두 개가 함께 쓰려고 React 바깥에 따로 만든 것이라, 값이 바뀌어도 React가 모른다. 그래서 명단으로 「바뀌었다」고 React에 알려 준다. 이 글에서는 「이 버튼을 MVVM으로 짰다면」을 그려 보며 공부한다.
먼저: 앞의 세 글에서 가져오는 것
이 글은 앞의 세 글과 계속 비교한다. 그 글들을 읽지 않아도 따라올 수 있게 필요한 것만 추린다.
MVC 는 화면 코드를 Model(데이터와 규칙) · View(화면) · Controller(입력 받기)로 나눈다. 핵심 장치는 「Model이 바뀌면 View들에 알리고, View가 다시 그린다」다. MVC가 풀려던 문제는 한 값이 여러 곳에 보이고(보여주는 곳이 여럿) 그 값이 여러 길로 바뀔 때(바뀌는 길이 여럿), 「곳 × 길」만큼 손으로 챙기다 빠뜨리면 화면끼리 숫자가 어긋나는 것이었다.
Flux 는 은행으로 공부했다. 통장 담당(Store)이 좋아요 담당, 합계 담당처럼 업무마다 여럿이고, 창구가 받은 전표를 그 여러 담당에게 나눠 준다. MVC 방식은 누구든 통장을 펼쳐 잔고를 직접 고쳐 쓰는 은행이라, 잔고가 이상해도 누가 고쳤는지 모른다. Flux 방식은 잔고를 바꾸려면 「3만 원 출금」 같은 전표를 창구에 내야 하고, 통장 담당이 전표를 보고 직접 고치는 은행이다. 이름으로는 전표가 Action, 창구가 Dispatcher, 값을 들고 있는 통장 담당이 Store다. 값이 바뀌는 길이 전표 하나뿐이고 한쪽으로만 흘러서 「한 방향 흐름」이라고 한다. Store는 화면을 모르고, 화면이 Store에 「바뀌면 알려 달라」고 미리 등록해 둔다. 통장을 고치는 사람이 나 혼자면 이 전표 제도는 서류만 늘린다는 것도 Flux 글에서 확인했다.
Redux 는 Flux를 더 단순하게 만들었다. Store를 하나로 합쳐 창구가 필요 없어졌고, 전표는 store.dispatch(전표)로 Store에 바로 낸다. 값을 고쳐 쓰지 않고, Reducer라는 함수가 바뀐 값을 담은 새 상태 객체를 만든다. 값이 바뀌면 새 객체가 생기고, 안 바뀌면 이전 객체를 그대로 쓴다. 그래서 「바뀌었나」는 두 객체가 같은 객체인지 == 한 번만 보면 알 수 있다. 들어온 전표는 글자와 숫자 같은 값만 든 평범한 객체라서, 그대로 적어 둘 수 있다. Redux에는 브라우저에 설치하는 개발 도구가 있어서, 들어온 전표와 그때마다 만들어진 상태를 목록으로 모아 순서대로 보고 지난 상태로 되돌려 볼 수 있다.
이 글의 자바스크립트 예제를 읽으려면 문법 셋만 알면 된다.
{ type: '별점_선택됨', rating: 5 }: 클래스 없이 중괄호로 바로 만드는 객체다.이름: 값짝을 늘어놓는다. 자바의Map에 값을 넣어 둔 것과 비슷하다{ type: '닉네임_입력됨', value }: 이름만 있고 콜론이 없는value는value: value를 줄여 쓴 것이다. 같은 이름의 변수 값을 그대로 넣는다{ ...state, nickname: 새 값 }:...state는state의 값을 전부 베낀다는 뜻이다. 베낀 뒤nickname만 바꾼 새 객체를 만든다
MVVM의 세 부품은 한 줄로 서서 앞만 안다
Microsoft 문서가 세 부품의 관계를 한 문장으로 정리했다.
At a high level, the view “knows about” the view model, and the view model “knows about” the model, but the model is unaware of the view model, and the view model is unaware of the view.
크게 보면 View는 ViewModel을 「알고」, ViewModel은 Model을 「안다」. 하지만 Model은 ViewModel을 모르고, ViewModel은 View를 모른다.
그림으로 그리면 한 줄이다.
View ──안다──▶ ViewModel ──안다──▶ Model
◀──모른다── ◀──모른다──
세 부품을 좋아요 버튼에 대면 이렇다.
- Model: 앱의 데이터와 업무 규칙이다. 서버에 저장된 좋아요 수와 「좋아요 올리기」 규칙이 여기 속한다. 화면을 전혀 모른다
- ViewModel: View가 보여줄 값을 화면에 바로 쓸 모양으로 준비해 들고 있는 객체다. 좋아요 수
count = 13, 눌렀는지liked = true, 그리고 화면에 찍을 글자"13명이 좋아해요"까지 들고 있다 - View: 화면이다. 하트 모양과 숫자다. ViewModel의 값을 묶어서 보여준다
「화면에 바로 쓸 모양으로 준비한다」가 ViewModel의 특징이다. 문서도 이렇게 적었다.
Each view model provides data from a model in a form that the view can easily consume. To accomplish this, the view model sometimes performs data conversion.
각 ViewModel은 Model의 데이터를 View가 쓰기 쉬운 모양으로 내준다. 그러려고 ViewModel이 데이터를 변환하기도 한다.
공부하며 받은 확인 문제가 「Model에는 count: 13만 있다. 화면에 ‘13명이 좋아해요’라는 글자를 만드는 일은 어디서 하나」였고, 나는 ViewModel이라고 답했다. 맞았다. Model은 숫자만 들고, 글자는 ViewModel이 만들고, View는 그 글자를 계산 없이 그대로 찍는다.
이렇게 나누면 얻는 게 있다. 문서가 MVVM의 이득으로 꼽은 것 중 하나다.
Developers can create unit tests for the view model and the model, without using the view.
개발자는 View 없이 ViewModel과 Model의 단위 테스트를 만들 수 있다.
화면을 띄우지 않고 ViewModel만 만들어서 「count가 13이면 "13명이 좋아해요"가 나오나」를 확인할 수 있다. 화면을 그리는 코드가 계산을 들고 있으면 이 테스트를 하려고 화면까지 띄워야 한다.
데이터 바인딩은 「이 칸은 이 값」이라고 적어 두면 도구가 맞춰 주는 장치다
MVVM에서 View와 ViewModel은 선 하나로 묶여 있다. 이걸 데이터 바인딩이라고 한다. 엑셀로 보면 바로 잡힌다.
엑셀 B1 칸에
=A1이라고 적어 두면, A1을 13으로 바꾸는 순간 B1도 저절로 13이 된다. B1을 다시 고쳐 쓰는 코드는 아무도 안 썼다.
MVVM의 View가 이렇다. View에 「이 칸에는 ViewModel의 count를 보여준다」라고 한 번 적어 두기만 하면, count가 바뀔 때 화면 숫자가 저절로 바뀐다.
MVC와 나란히 놓으면 차이가 보인다.
- MVC: View가 Model에 「바뀌면 알려 줘」라고 등록하고, 알림이 오면 View 코드가 직접 숫자를 다시 그린다
- MVVM: View에 「이 칸 =
count」라고 적어만 두면, 알림을 받고 다시 그리는 일을 도구가 대신 한다. 여기서 도구는 Vue나 .NET MAUI처럼 데이터 바인딩 기능을 가진 화면 도구다. 바인딩을 적어 두면 그 도구가 등록과 다시 그리기를 맡는다
공부하며 받은 첫 확인 문제는 「ViewModel은 View를 모른다. 그럼 count가 13이 됐을 때 화면은 어떻게 13으로 바뀌나」였다. 나는 「View가 ViewModel을 안다고 했으니, 변하면 자동으로 변하는 거 아니야?」라고 답했다. 방향은 맞았고, 그 「자동으로」 속을 채우면 이렇다. Microsoft 문서가 ViewModel의 일을 이렇게 적었다.
The view model implements properties and commands to which the view can data bind to, and notifies the view of any state changes through change notification events.
ViewModel은 View가 데이터 바인딩할 수 있는 속성과 명령을 구현하고, 상태가 바뀌면 변경 알림 이벤트로 View에 알린다.
속성은 count나 liked 같은 값이고, 명령은 「좋아요 누르기」처럼 버튼을 눌렀을 때 실행할 동작이다. 명령도 ViewModel에 메서드처럼 두고, 화면의 버튼을 그 명령에 묶는다. 그러면 버튼 쪽에 클릭 처리 코드를 따로 쓰지 않아도 된다.
In order for the view model to participate in two-way data binding with the view, its properties must raise the
PropertyChangedevent.ViewModel이 View와 양방향 데이터 바인딩을 하려면, 속성이 바뀔 때
PropertyChanged이벤트를 내야 한다.
양방향 바인딩은 뒤에서 자세히 본다. 여기서는 이렇게만 알아 두면 된다. PropertyChanged가 맡는 건 「ViewModel → 화면」 방향이다. 반대 방향 「화면 → ViewModel」은 바인딩 도구가 맡는다. 사용자가 입력 칸을 바꾸면 도구가 그 값을 ViewModel 속성에 직접 넣는다. 그러면 그 속성이 다시 PropertyChanged를 내서, 같은 값에 묶인 다른 칸들도 따라 바뀐다. 그래서 양방향이라도 이 이벤트가 필요하다.
그러니까 속에서는 이런 일이 일어난다.
- ViewModel은
count가 바뀌면 「count바뀌었어」라고 외치기만 한다(PropertyChanged이벤트). 누가 듣는지는 모른다 - View 쪽 바인딩은 화면이 처음 만들어질 때 그 외침을 듣겠다고 미리 등록해 둔다
- 외침이 들리면 바인딩이
count를 다시 읽어 숫자를 고친다
이건 앞에서 추린 Flux의 「Store는 화면을 모르고, 화면이 Store에 등록해 둔다」와 같은 모양이다. 모르는 쪽은 외치기만 하고, 아는 쪽이 미리 등록해서 듣는다. MVVM에서는 그 등록과 다시 그리기를 개발자가 아니라 바인딩 도구가 해서 「자동」처럼 보인다. 속에서는 MVC 글에서 본 「바뀌면 알린다」 장치가 그대로 돌고 있다.
양방향 바인딩은 화면 입력까지 값에 저절로 넣어 준다
지금까지 본 바인딩은 한 방향이었다. ViewModel이 바뀌면 화면이 바뀐다. MVVM은 반대 방향까지 묶을 수 있다. 후기를 쓰는 화면에 「별점」 입력 칸이 있다고 하자.
ViewModel.rating ◀──── 양방향 ────▶ 화면의 별점 입력 칸
- ViewModel에서
rating을 4로 바꾸면, 화면의 별이 4개가 된다 - 사용자가 화면에서 별 5개를 누르면, ViewModel의
rating도 저절로 5가 된다
입력 칸의 값을 꺼내 ViewModel에 넣는 코드를 아무도 쓰지 않았는데 들어간다. 이게 양방향 바인딩이다. 정말 편하다.
양방향으로 많이 묶으면 연쇄 갱신으로 누가 값을 바꿨는지 알 수 없게 된다
화면이 커지면 별점 하나에 여러 값이 묶인다.
사용자가 별 5개 클릭
→ rating이 5가 됨 (양방향 바인딩)
→ rating을 보고 「추천 문구」가 「최고예요!」로 바뀜
→ 추천 문구가 바뀌면 「제출 버튼」이 켜짐
→ 제출 버튼이 켜지면 「안내 문구」가 바뀜
→ ...
값 하나를 바꿨는데 다른 값이 바뀌고, 그게 또 다른 값을 바꾼다.
그런데 이런 연쇄는 한 방향 구조에서도 생긴다. Redux에서도 별점이 바뀌면 추천 문구와 버튼이 줄줄이 다시 계산될 수 있다. 양방향이 문제를 키우는 지점은 연쇄가 어디서 시작되느냐다. 양방향으로 묶인 칸은 모두 값을 바꾸는 입구가 된다. 별점 칸, 추천 문구를 고르는 칸, 다른 화면의 별점 표시까지 양방향으로 묶여 있으면, 그중 어느 칸에서든 사용자가 손대는 순간 값이 바뀌고 연쇄가 시작된다. 게다가 값이 바뀌면 묶인 칸이 바뀌고, 그 칸이 또 양방향으로 다른 값에 묶여 있으면 그 값이 다시 바뀌는 식으로 값 → 화면 → 값으로 되돌아오는 고리도 생긴다.
그래서 묶인 선이 많아질수록 「이 값이 왜 이렇게 됐지?」를 찾으려면 선을 전부 거꾸로 따라가야 한다. 화면의 어느 칸에서 사용자가 바꾼 건지, ViewModel 코드가 바꾼 건지도 헷갈린다. Redux에서는 연쇄가 생겨도 출발점이 언제나 전표 하나이고, 그 전표가 기록에 남는다.
이게 Flux 글에 옮긴 Flux 문서의 문장 그대로다.
We found that two-way data bindings led to cascading updates, where changing one object led to another object changing, which could also trigger more updates.
양방향 데이터 연결은 연쇄 갱신을 낳았다. 객체 하나를 바꾸면 다른 객체가 바뀌고, 그게 또 다른 갱신을 일으켰다.
Flux 문서가 버린 「양방향 데이터 연결」이 바로 MVVM 같은 구조의 핵심 장치였다. 그러니 순서가 이렇게 이어진다.
MVC : 역할을 셋으로 나눈다. Model이 바뀌면 View에 알린다
MVVM : View와 ViewModel을 양방향으로 저절로 묶는다 → 편하지만 연쇄 갱신
Flux : 양방향을 버리고 한 방향 흐름 하나로 묶는다
Redux : Flux를 더 단순하게 한다. Store 하나, 새 상태 객체
같은 일을 Redux로 하면 화면이 전표를 직접 내야 한다
다음 확인 문제는 「양방향 바인딩에서는 별 5개를 누르면 rating이 저절로 5가 된다. 같은 일을 Redux로 하면 화면은 무엇을 해야 하나」였다. 나는 「화면이 전표를 만들어서 디스패치에 전달한다」고 답했다. 맞았고, 하나만 다듬었다. Redux에는 Flux에 있던 창구(Dispatcher)가 따로 없어서 전표를 Store에 직접 낸다. store는 앱에 하나뿐인 Redux Store를 담은 변수다.
store.dispatch({ type: '별점_선택됨', rating: 5 });
이 줄은 「별점이 5로 선택됐다」는 전표(Action)를 Store에 낸다. 그러면 Store가 Reducer로 rating: 5가 든 새 상태 객체를 만들고, 「바뀌었다」고 알리고, 화면이 다시 그려진다. MVVM에서는 누르기만 하면 값이 저절로 바뀌었는데, Redux에서는 화면이 「무슨 일이 있었다」는 전표를 직접 내야 한다. 이 차이가 다음 섹션의 판단으로 이어진다.
입력 칸 두세 개짜리 작은 화면은 MVVM이 편하다
그다음 문제는 「입력 칸 두세 개짜리 작은 설정 화면이면 MVVM과 Redux 중 어느 쪽이 편한가」였다. 나는 감이 오지 않았다. 비교할 재료가 없었기 때문이다. 그래서 「닉네임」과 「알림 받기」 두 칸짜리 설정 화면을 두 방식으로 나란히 써 봤다. 실제 문법이 아니라 모양만 보는 코드다.
MVVM(양방향 바인딩)으로 쓰면 이렇다.
ViewModel: nickname = "", notify = true
화면:
[닉네임 입력 칸] ⟷ nickname 에 묶기
[알림 체크 칸] ⟷ notify 에 묶기
끝이다. 사용자가 글자를 치면 nickname이 저절로 바뀌고, 체크를 누르면 notify가 저절로 바뀐다. 칸마다 「묶기」 한 줄이다.
Redux(한 방향)로 쓰면 이렇다.
전표 종류 정하기: '닉네임_입력됨', '알림_바뀜'
Reducer:
'닉네임_입력됨'이 오면 → 새 상태 { ...state, nickname: 전표의 value }
'알림_바뀜'이 오면 → 새 상태 { ...state, notify: 전표의 value }
화면:
[닉네임 입력 칸] 글자가 바뀔 때마다 → dispatch({ type: '닉네임_입력됨', value })
[알림 체크 칸] 누를 때마다 → dispatch({ type: '알림_바뀜', value })
그리고 Store의 값을 읽어서 칸에 보여주기
화면 쪽의 value는 그 순간 입력 칸에 들어 있는 글자(또는 체크 칸의 켜짐·꺼짐)다. 화면이 그걸 전표에 담아 내면, Reducer가 전표의 value를 꺼내 새 상태에 넣는다.
같은 일을 하는데 전표 종류, Reducer 갈래, 칸마다 dispatch까지 써야 한다. 칸이 하나 늘 때 MVVM은 「묶기」 한 줄이 늘고, Redux는 전표 종류 하나, Reducer 갈래 하나, dispatch 한 줄이 는다.
그리고 칸이 두 개면 값을 바꾸는 길도 두 개뿐이다. 추적할 게 없으니 Redux의 장점인 전표 기록은 쓸 데가 없고, 쓸 코드만 늘어난다. Flux 글에서 「통장을 고치는 사람이 나 혼자면 전표 제도는 서류만 늘린다」고 했던 것과 같은 판단이다. 작은 화면은 MVVM이 편하다.
여러 화면이 같이 보고 고치는 값은 Redux가 맞다
감을 잡으려고 쉬운 문제 둘을 더 받았다. 회원가입 폼(이메일과 비밀번호 두 칸, 이 화면에서만 쓰고 제출하면 끝난다)과 쇼핑몰 장바구니(상품 목록, 상품 상세, 맨 위 아이콘의 「담은 개수」, 결제 화면이 모두 같은 장바구니를 보고 고친다)다. 나는 회원가입 폼은 MVVM, 장바구니는 Redux라고 답했다. 장바구니에는 「이게 Redux라고?」 하고 물음표를 붙였다.
장바구니가 Redux 쪽인 이유는 보고 고치는 곳을 세어 보면 나온다.
상품 목록 화면 ──「담기」──┐
상품 상세 화면 ──「담기」──┤
결제 화면 ──「빼기」──┼──▶ 장바구니 (하나)
│
맨 위 아이콘 ◀──「담은 개수 3」 보여주기
장바구니를 보는 곳은 넷이고, 그중 고치는 화면은 셋(목록, 상세, 결제)이다. 맨 위 아이콘은 보여주기만 한다. 이걸 화면마다 자기 ViewModel이 장바구니 값을 따로 들고 양방향으로 묶는 식으로 짜면, 고치는 세 화면이 각자 자기 복사본을 직접 고친다. 복사본이 여럿인데 서로 맞춰 주는 장치는 없다. 목록 화면에서 담은 것이 결제 화면의 복사본에는 안 들어가고, 아이콘은 또 다른 복사본을 보여준다. 어느 날 아이콘에 「담은 개수 5」가 떴는데, 사용자가 실제로 담은 건(서버에 저장된 장바구니로는) 3개다. 어느 화면이 언제 고쳤는지 알 수 없다. MVC 글의 「보여주는 곳 × 바뀌는 길」이 딱 이 상황이다.
MVVM으로도 이걸 피할 수는 있다. 장바구니 ViewModel을 하나만 두고 네 곳이 같이 쓰게 하면서, 장바구니는 그 ViewModel의 「담기」·「빼기」 명령으로만 고치게 하면 된다. 그런데 그렇게 하면 「값은 한 곳에 두고, 정해진 입구로만 고친다」가 되어 Redux가 하려던 것과 같은 방향이 된다. Redux는 그걸 처음부터 규칙으로 정해 둔 도구다.
Redux로 하면 고치는 세 화면이 모두 전표만 낸다.
상품 목록: dispatch({ type: '장바구니_담음', 상품번호: 12 })
상품 상세: dispatch({ type: '장바구니_담음', 상품번호: 12 })
결제 화면: dispatch({ type: '장바구니_뺌', 상품번호: 7 })
장바구니를 실제로 고치는 곳은 Reducer 한 곳뿐이다. 개수가 이상하면 들어온 전표를 순서대로 보면 원인이 나온다. 보는 곳과 고치는 곳이 많을수록 Redux의 서류 작업이 제값을 한다.
그리고 실제 앱은 둘 중 하나만 고르지 않는다. 회원가입 폼처럼 한 화면 안에서만 쓰고 끝나는 값은 그 화면에 묶어 두고, 장바구니처럼 여러 화면이 같이 쓰는 값만 Redux의 Store 하나에 모은다. 그래서 판단 기준은 「이 앱이 MVVM이냐 Redux냐」가 아니라 「이 값을 몇 군데서 보고, 몇 군데서 고치나」다. Redux 공식 FAQ도 같은 선을 긋는다. 이 이야기는 Redux 글에 따로 정리했다.
MVC · MVVM · Redux와 FSD는 위아래가 아니라 서로 다른 축이다
여기까지 공부하고 나서 이렇게 물었다. 「그럼 MVC, MVVM, Redux는 동급의 선택 항목이고, FSD는 상위 항목이구만?」 반은 맞았다.
맞은 쪽: MVC, MVVM, Redux는 같은 질문에 답하는 선택지다. 셋 다 「값이 어떻게 바뀌고, 그게 화면에 어떻게 반영되나」에 대한 서로 다른 답이다. MVC는 Model이 바뀌면 View에 알리고, MVVM은 View와 ViewModel을 묶어 저절로 맞추고, Redux는 전표 하나로만 바꿔 한 방향으로 흘린다. 다만 앞 섹션처럼 한 앱 안에서 섞어 쓴다.
다른 쪽: FSD는 「상위」가 아니라 「다른 축」이다. FSD는 「파일을 어느 폴더에 두고, 누가 누구를 불러 쓸 수 있나」에 답한다. 값이 어떻게 흐르는지와는 다른 질문이다. 집 짓기로 보면 이렇다.
FSD는 방 배치다. 부엌은 어디, 침실은 어디, 어느 방에서 어느 방으로 문이 나 있는지를 정한다. MVC · MVVM · Redux는 배관이다. 물이 어디서 들어와 어떻게 흐르는지를 정한다.
배관은 방 안을 지나간다. 그렇다고 방 배치가 배관의 윗단계는 아니다. 방 배치를 바꿔도 배관 방식은 그대로일 수 있고, 배관을 바꿔도 방 배치는 그대로일 수 있다. 서로 따로 고르는 두 가지다. 실제로 Redux 글에서 FSD 문서에 Redux가 들어갈 자리가 이미 있다는 걸 확인했다. Store를 만드는 코드는 app/store/에, 후기 Reducer는 entities/review/model/에 둔다.
그림으로 그리면 위아래가 아니라 가로세로다.
값이 흐르는 방식 (배관) →
MVC MVVM Redux
폴더 구조 FSD ○ ○ ○
(방 배치) 종류별 ○ ○ ○
↓ ...
그림의 「종류별」은 components/(화면 조각), hooks/(React에서 상태를 다루는 함수인 훅 모음), api/(서버 호출)처럼 파일의 종류로 폴더를 나누는 흔한 구조다. 어떤 폴더 구조든 어떤 흐름 방식이든 짝지을 수 있다. 그래서 FSD와 Redux가 함께 쓰일 수 있었다.
판단 기준 정리
| 질문 | 답 | 근거 |
|---|---|---|
| MVVM의 세 부품은 서로 아나 | View는 ViewModel을, ViewModel은 Model을 안다. 거꾸로는 모른다 | Microsoft 문서 |
| ViewModel은 무엇을 하나 | 값을 화면에 바로 쓸 모양으로 준비한다 | 「13」을 「13명이 좋아해요」로 만든다. 화면 없이 테스트할 수 있다 |
| 화면은 어떻게 저절로 바뀌나 | ViewModel이 PropertyChanged로 알리고, 바인딩이 미리 등록해 듣는다 |
Flux의 「Store는 화면을 모르고 화면이 등록한다」와 같은 모양 |
| 양방향 바인딩은 무엇인가 | 화면 입력도 ViewModel 값에 저절로 들어간다 | 입력을 옮기는 코드를 안 써도 된다 |
| 양방향 바인딩의 대가는 | 커지면 연쇄 갱신으로 누가 바꿨는지 알 수 없다 | Flux가 버린 「two-way data bindings」 |
| 작은 화면은 | MVVM | 칸 하나에 묶기 한 줄. 추적할 길이 없다 |
| 여러 화면이 같이 쓰는 값은 | Redux | 고치는 곳이 Reducer 하나로 모이고 전표로 남는다 |
| MVC · MVVM · Redux와 FSD의 관계는 | 다른 축이다 | 흐름 방식(배관)과 폴더 구조(방 배치)는 따로 고른다 |
이 정리에 닿기까지
시작은 이름을 잘못 기억한 질문이었다. MVC, Flux, Redux 다음에 「MVVC인가? 그런 것도 있지 않아?」라고 물었고, 말하려던 것이 MVVM이었다. FSD 글에서 이름만 지나갔던 후보였다.
출처부터 확인했다. Microsoft의 .NET 문서와 Vue 2 문서를 원본으로 받아 읽었다. 누가 처음 만들었는지는 어디에도 없어서 그 이야기는 뺐다.
세 부품의 「누가 누구를 아나」에서 Flux와 같은 모양을 봤다. ViewModel은 View를 모르는데 화면이 어떻게 바뀌냐는 문제에 「View가 아니까 자동으로」라고 답했다. 그 자동의 속은 「모르는 쪽이 외치고, 아는 쪽이 미리 등록해 듣는다」였고, Flux에서 Store와 화면 사이에 본 것과 같았다.
양방향 바인딩에서 Flux가 왜 나왔는지가 이어졌다. 별점 하나에 값이 줄줄이 묶이는 장면을 그리자, Flux 문서가 버린 「양방향 데이터 연결」이 바로 이것이라는 게 보였다.
작은 화면에 무엇이 편한지는 감이 오지 않았다. 두 칸짜리 설정 화면을 MVVM과 Redux로 나란히 써 보고서야 보였다. 칸이 적으면 추적할 길도 적어서 Redux의 서류 작업은 짐이다. 회원가입 폼과 장바구니로 다시 확인했고, 장바구니가 Redux라는 데는 물음표를 붙였다가 보고 고치는 곳 넷을 세어 보고 납득했다.
마지막으로 네 가지를 한 그림에 놓았다. MVC, MVVM, Redux는 같은 줄의 선택지이고 FSD는 상위라고 생각했는데, FSD는 위가 아니라 옆이었다. 흐름 방식과 폴더 구조는 가로세로로 따로 고르는 두 축이다.
정리
- MVVM은 View · ViewModel · Model이 한 줄로 서서 앞만 아는 구조다. ViewModel은 값을 화면에 바로 쓸 모양으로 준비한다
- 데이터 바인딩은 「이 칸은 이 값」이라고 적어 두면 도구가 맞춰 준다. 속에서는 「바뀌면 알린다」 장치가 그대로 돈다
- 양방향 바인딩은 편하지만, 커지면 연쇄 갱신으로 누가 값을 바꿨는지 알 수 없다. 그게 Flux가 피하려던 문제다
- 작은 화면은 MVVM, 여러 화면이 같이 보고 고치는 값은 Redux다. 한 앱 안에서 섞어 쓰고, 기준은 「이 값을 몇 군데서 보고 고치나」다
- MVC · MVVM · Redux와 FSD는 다른 축이다. 흐름 방식(배관)과 폴더 구조(방 배치)는 따로 고른다
자신만의 철학을 만들어가는 중입니다.
댓글남기기