-
[React] React Architecture 정리FrontEnd/React 2023. 3. 7. 09:56
1. MVC 아키텍처
Model(데이터) - View(화면) - Controller(컨트롤러)
- Model의 데이터를 받아서 화면에 그리고, 화면으로부터 사용자의 동작을 받아 Model을 변경
- Ajax로 부터 받는 데이터를 Model
- HTML과 CSS로 만들어지는 화면을 View
- javascript가 데이터를 받아 조작하여 서버에 전달하는 Controller의 역할을 수행
- Model과 View의 종속성을 최대한 분리하기 위해 HTML과 jQuery를 따로 관리하는 것이 주효
- Ajax를 통해 서버에서 HTML을 만들지 않고 데이터만 교환이 가능하게 되었고 JS로 DOM을 조작하는 작업이 중요함에 따라 jQuery를 통해 Ajax와 DOM을 다루게 됨
2. MVVM 아키텍처
선언적(Declarative) 프로그래밍
명령형(절차적) 프로그래밍은 당신이 어떤 일을 어떻게 할 것인가에 관한 것이고, 선언적 프로그래밍은 당신이 무엇을 할 것인가에 관한 것이다.
명령형 접근 방식(HOW):
"OO님 디자인팀에서 디자인해준 페이지를 기반으로 페이지를 만든다음에, 데이터베이스에서 전체 유저 리스트에 대해, AWS ses를 통해 메일을 보내는 스크립트를 만들어 실행해주세요."선언형 접근 방식(WHAT):
"OO님 프로모션 메일 보내주세요."// 명령형 방식 (HOW) function double(arr){ let results = []; for(let i=0; i<arr.length; i++){ results.push(arr[i] * 2) } return results; } // 선언형 방식 (WHAT) function double(arr){ return arr.map((item) => item * 2) }
명령형 방식에서 for문 내부의 변수가 외부로부터 노출되지 않도록 map이란 함수로 캡슐화한다. 각각의 함수를 목적에 맞게 나누고 최소한의 역할만을 수행하도록 구현하여 함수의 조합으로 프로그래밍을 설계하게 되면 각 함수의 유지보수가 쉬워지고 재사용하기 쉬워진다.
Frontend를 이루는 언어들은 선언적 방식으로 작동하는데, 예를 들어 HTML에서 header와 body와 같이 태그를 이용하는 것은 어떻게 화면에 보여질 지를 작성하는 것이 아닌, 무엇을 화면에 보여주어야 하는지 작성하는 것입니다.
선언형 프로그래밍에서는 내부적으로는 절차적 알고리즘이 동작하고 있고 절차적인 명령형 알고리즘이 추상화 되어서 선언형 프로그래밍으로 제공되는 것을 볼 수 있다.
앵귤러에서는 MVC 구현 방식을 jQuery와 같은 DOM 조작에서 템플릿과 바인딩을 통한 선언적인 방법으로 구현하였다. 코드에서 DOM을 조작하는 코드가 사라지고 이 기능을 프레임워크가 담당하게 되었으며 개발자는 데이터를 프레임워크에 전달해주면 프레임워크가 알아서 그려준다.
이를 View를 그리는 Model만 다루게 되었다는 의미로 ViewModel이라고 부르며 이 방식을 MVVM이라고 부르게 된다.
MVC에서 MVVM으로 오면서 달라진 부분
- 컨트롤러의 반복적인 기능이 선언적인 방식으로 개선이 되었다.
- Model과 View의 관점을 분리하려 하지 않고 하나의 템플릿으로 관리하려는 방식으로 발전했다. (기존에는 class나 id등으로 간접적으로 HTML에 접근하려고 했다면 이제는 직접적으로 HTML에 접근하는 방법으로 확장이 되었다.)
3. Component 패턴 그리고 Container-Presenter 패턴
웹 서비스의 발전에 따라 화면단위가 아니라 더 작게 재사용할 수 있도록 컴포넌트로 만들어 조립할 수 있는 방식으로 발전 하게 되었고 이를 Component 패턴이라 한다.
컴포넌트는 재사용이 가능해야 한다는 원칙에 따라 비즈니스 로직을 관장하고 있는 컴포넌트를 Container 컴포넌트로, 데이터만 뿌려주는 컴포넌트를 Presenter 컴포넌트로 분리하여 Container-Presenter 아키텍처가 만들어졌다.
하지만 컴포넌트 구조가 복잡해지면서 상단에 있는 데이터를 하단으로 보내기 위해 props로 계속 전달해 주어야 했고 이렇게 수많은 props가 생겨나는 Props Drilling Problem 문제가 생겨났다.
4. FLUX 패턴과 Redux
컴포넌트의 재사용과 독립성을 지나치게 강조하다 보니 Component간 데이터 전달이 어렵고 Model의 관리가 파편화 되는 문제가 발생하였다.
그래서 FLUX라는 단일 흐름을 만들자는 주장이 생겨났다.
View에서 Action을 호출하면 Dispatcher를 통해 Store에 Data가 보관이 되고 이는 다시 View로 전달된다.
그리고 Redux가 탄생하게 된다.
기존의 컴포넌트 단위의 MVC개념에서 완전히 비지니스 로직과 View를 분리하면서 이 분리된 개념을 상태관리(State Management)라고 부르게 된다.
MVC에서 FLUX으로 오면서 달라진 부분
- 공통적으로 사용되는 비지니스 로직의 Layer와 View의 Layer를 완전히 분리되어 상태관리라는 방식으로 관리한다.
- 각각의 독립된 컴포넌트가 아니라 하나의 거대한 View 영역으로 간주한다.
- 둘 사이의 관계는 Action과 Reduce라는 인터페이스로 분리한며 Controller는 이제 양방향이 아니라 단반향으로 Cycle을 이루도록 설계한다.
5. Hooks와 Context, Recoil, Zustand, jotai
간결한 문법과 외부에서 데이터를 사용할 수 있도록 Hooks
Props Drilling 없이 상위 props를 하위로 전달할 방법을 제공하도록 Context
Atom이라고 불리는 전역객체를 이용해 데이터를 기록하고, 변경감지를 통해 View로 전달하는 Recoi, Zustands, Jotai 방식
6. React Query, SWR, Redux Query
웹의 특성상 대부분의 비즈니스 로직은 백엔드에서 보관이 되고 프론트엔드에서는 이를 호출하는 방식을 사용한다.
백엔드와 직접 연동해 기존 상태관링에서 로딩, 캐싱, 무효화, 업데이트 등 복잡하게 진행하던 로직들을 단순하게 만들어지는 방식이 생겨났다.
즉, 서버의 API를 관리하는 도구들이 위의 라이브러리들이다.
7. MVI
Model - View - Intent
사용자가 +버튼을 누르거나 위쪽 방향 키보드를 눌렀을 때 동일하게 숫자를 증가시키는 로직이 있다고 할때, 사용자가 한 행동은 다르지만 데이터의 입장에서는 같은 동작을 수행한 것이다.
즉, 비즈니스 로직은 다음과 같은 2가지 레이어로 분할 할 수 있다.
1) 사용자가 View를 통해서 전달한 UI Event를 어떠한 데이터 변화를 하게 할지 전달하는 역할
2) 전달받은 요청에 따라서 적절히 데이터를 변화하는 역할
MVI 아키텍처가 MVC나 MVVM과는 다른 점은 각각의 컴포넌트에 한정되는 것이 아니라 앱 전체에 적용이 된다는 것이다.
- 데이터가 단방향으로 순환한다. => 데이터의 흐름을 이해하고 디버깅을 하기 쉬워진다.
- 비즈니스 로직이 View에 의존하지 않는다. => UI 변화 요구사항에 유연하게 대응할 수 있다.
- View의 생명주기와 무관하게 일관성 있는 상태를 갖는다. => 컴포넌트 생명주기에 따른 상태 동기화 문제를 해결한다.
=> FLUX, Redux, Hooks, React Query 등 MVVM 이후 프론트엔드에서 제시되었던 방법론들의 총집합
8. 프론트엔드 아키텍처 변화 정리
프론트엔드는
1) 데이터를 화면으로 변경하는 작업을 자동화하는 방향
=> MVC, MVVM
2) 페이지 단위에서 컴포넌트 단위로 작업 방식이 진화
=> Component
3) 컴포넌트의 재사용성과 독립성이 중요해지면서 컴포넌트를 분류
=> Container-Presenter Pattern
4) 컴포넌트의 계층 구조가 복잡해지면서 데이터교환이 복잡해 지자 단방향 아키텍처 제기
=> FLUX
5) 데이터의 변화를 다루는 상태관리가 컴포넌트 프레임워크와 분리
=> Redux
6) 상태관리가 고도화되고 코드가 복잡해지면서 조금 더 간결하게 데이터의 변경감지를 전달할 수 있는 형태로 발전
=> Hooks와 Context, Recoil, Zustand, jotai
7) 전역적인 데이터 스토어 방향과 서버 상태를 조금 더 편하게 다룰 수 있는 방향으로 발전
=> React Query, SWR, Redux Query
의 방향으로 진화해왔다.
9. 그래서 우리 프로젝트는.....?
1) Redux
// View에서 데이터 처리를 하는 경우 const onClick = () => { setVisible(!visible) setCount(count+1) setTodos([...todos, {title: "할일추가"}) }
데이터가 변화하는 View 코드에 흩어져 있다면 어떤 식으로 데이터가 변화하는지 추적하는데 어려움을 겪는다.
또한 View에서 직접 데이터를 수정하도록 작성한다면 모델과 View 간의 의존성이 심화된다.
count state가 props로 전달되며 view 이곳 저곳에서 사용되고, 각각의 view에서 변화하고 있다면 추적하기 어려울 것이다.
const liveSlice = createSlice({ name: "live", initialState, reducers: { addLiveMatch(state, action) { state.liveList.push(action.payload); }, clearAllLiveMatch(state, action) { state.liveList = []; } }, })
위와 같이 model에서 데이터 조작을 하고, view에서는 useDispatch()와 useSelect() 사용자 입출력에만 신경쓰면 데이터 관리에 용이할 것이다.
전역적으로 상태관리를 하며, view에서 데이터 조작을 하는 것이 아니라 모델의 reducer에서 데이터 처리를 하는 것을 목표로 한다.
2) React-Query
// useLiveMatchListQuery hook을 만들어서 필요할 때 호출한다. import { SERVER_URL } from '@/utils/url'; import axios from 'axios'; import { useQuery } from 'react-query'; export const LIVE_MATCH_LIST = '/live'; const fetcher = (lat: number, lng: number) => axios.get(SERVER_URL + '/live', { params: { lat: lat, lng: lng } }).then(({ data }) => data) // 좌표를 받아왔을 때만 query const useLiveMatchListQuery = (lat: number, lng: number) => { return useQuery(LIVE_MATCH_LIST, () => fetcher(lat, lng), { staleTime: 30 * 1000, cacheTime: 60 * 5 * 1000, refetchInterval: 30 * 1000, enabled: lat != null, refetchOnWindowFocus: true }); } export default useLiveMatchListQuery;
위와 같이 custom hook을 만들어 서버와 비동기적 데이터 통신이 필요할 때 독립적으로 호출하여 사용할 수 있다.
서버와의 비동기 통신은 custom hooks를 만들어 React-Query를 이용해 필요할 때마다 호출할 수 있도록 따로 관리한다.
3) Custom Hooks
서버와 API 통신 이외에도 반복되는 작업이나, 복잡한 함수는 custom hook을 이용하여 숨기고 선언적으로 프로그래밍을 할 수 있도록한다.
복잡한 데이터 조작은 custom hooks를 이용하여 숨긴다. custom hooks는 재사용에도 유리하다.
4) Atomic Components
자주 사용되는 버튼, Nav Bar, 리스트 형식 등은 재사용할 수 있도록 최하위 계층의 view만을 담당하는 component로 생성하여 재사용에 용이하도록 한다.
이 때, tailwindCSS만으로는 이러한 component 구성이 쉽지 않다. 예를 들어 button의 배경색, 테두리 굵기, round는 일정하지만 width와 height만 동적으로 변하는 button이 있다고 할때
function ExampleDiv({ width, height }) { return <div class=`bg-black w-[${width}px] h-[${height}px]`>안녕하세요</div>; // X }
다음과 같이 변수를 넣어줄 수는 없습니다.
그래서 요소 자체에 스타일이 노출되는 인라인 스타일을 사용해야 한다. 하지만 이를 최대한 배제하고 싶다.
import tw from "twin.macro"; function ExampleDiv({ width, height }) { return ( <div css={[ tw`bg-black`, { width: width, height: height, }, ]} > 안녕하세요 </div> ); }
Twin.Macro는 이와 같이 가변적인 인자를 사용해서 component를 구현하는데 도움을 준다.
정리하면,
- 컴포넌트는 독립적으로, 재사용가능하도록! => 한 컴포넌트에서 너무 많은 일을 하도록 하지 않게 분할한다.
- 지나친 컴포넌트의 의존성 타파는 독립적일 필요도 없고, 재사용하기 어려운 '거대한 컴포넌트(Massive-View-Controller)'를 낳는다. => 너무 잘게 쪼갤 필요는 없다.
- 재사용이 어려운 1회성의 컴포넌트라면 비즈니스 모듈에 의존적이더라도 언제든 교체가능한 방식으로 만드는 것이 낫다. => 재사용성이 떨어지는 기능이라면 모듈 변화시에 그냥 나중에 새로 다시 짜는게 나을 수 있다.
- 그래도 모델과 View간의 의존성은 낮추자! => 클라이언트 데이터 처리는 model의 reducer에서, 서버와의 통신은 react-query를 활용, custom hooks를 적극적으로 활용하여 view component에서는 최대한 view에 관련된 처리만 할 수 있도록!
- view component 구축할 때 tailwindcss와 twin.macro를 사용해 가변적으로 인자를 받자
출처
https://yozm.wishket.com/magazine/detail/1663/
https://fe-developers.kakaoent.com/2022/221013-tailwind-and-design-system/
'FrontEnd > React' 카테고리의 다른 글
[React] Three.js + CSS3d animation (1) 2023.04.24 [React] React-Query의 개념 및 예제 (0) 2023.03.02 [React] 페이지 이동시, 파라미터 전달 및 취득 방법 (2) 2023.02.20 [React] Vite 기반 프로젝트에서 동적 import로 이미지 불러오기 (0) 2023.02.11 [React] 좋은 Frontend Devloper가 되기 위한 정리 (1) 2023.02.11