리액트에선 다양한 아키텍처 패턴이 존재한다.
Custom Hooks, Container Component 등등 다양한 용어들을 들어본 적이 있을 것이다.
오늘은 이런 패턴들을 정리해보려고 한다.
아키텍처와 디자인 패턴을 많이 혼동해서 사용한다.
다트 플러터의 GetX의 MVVM 패턴, Spring Boot의 MVC 패턴.. 이런 것들은 아키텍처 패턴일까 디자인 패턴일까?
바로 아키텍처 패턴
이다.
아키텍처 패턴
은 전체적인 집을 짓는 방법이며, 디자인 패턴
은 벽돌이나 샷시들을 만드는 방법이다.
넓은 범위의 의미인 아키텍처 패턴
에는 폴더의 구조를 명시해놓은 경우도 있고, 폴더의 구조가 명시되어있지 않더라도 각 소스 코드의 class, function 마다 역할이 다르다고 명시되어있다.
예를 들면, MVVM 패턴에는 ViewModel과 Model, View가 각각 구분되어 폴더에 들어가고 소스코드도 하나의 View 에 1개의 소스 코드.. 이런식으로 정해져있는 것처럼 말이다.
좁은 범위의 의미인 디자인 패턴
에는 설계 자체에서 어떠한 문제가 있을 때 해결하는 방법이다.
예를 들면, 싱글톤 패턴, 팩토리 메서드 패턴, 추상 팩토리 패턴, 빌더 패턴, 프로토타입 패턴 등등이 있다.
아키텍처 패턴과는 달리 하나의 소스코드로 예제를 설명할 수 있으며, 애초에 전체 프로젝트의 구조를 설명하는 것이 아니라, 코딩테스트의 한 문제를 푸는 느낌으로 하나의 문제를 해결하는 방법일 뿐이다.
결론을 말하자면, 철근을 가져오고.. 공구리를 치고.. 이런 것은 하나하나 디자인 패턴으로 여러 디자인 패턴이 모여 하나의 프로젝트를 이룬다.
이런 프로젝트를 마지막으로 깔끔하게 정리하는 방법이 아키텍처 패턴이라고 보면 쉽다.
먼저 공구리와 벽돌을 먼저 치는 좁은 의미인 디자인 패턴부터 알아보자.
여기서는 자주 들어본 것들이 나올 것이다.
Custom Hooks는 리액트 컴포넌트의 로직을 재사용하기 위해 사용하는 함수이다.
이를 통해 컴포넌트 간에 로직을 공유할 수 있으며, 복잡한 상태 관리나 사이드 이펙트를 처리할 수 있다.
Custom Hooks는 컴포넌트의 코드 중복을 줄이고, 코드의 가독성과 유지보수성을 높이는 데 도움을 준다.
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then((response) => response.json())
.then((data) => {
setData(data);
setLoading(false);
})
.catch((error) => {
setError(error);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
위는 어떠한 url로 요청을 하고, 로딩 상태이거나 에러 상태도 함께 반환하는 커스텀 훅이다.
기존에는 state를 3개나 생성하고 useEffect로 감지를 하는 코드를 매 컴포넌트 마다 호출 해야하지만, 이렇게 커스텀 훅 useFetch()를 선언하면, API를 호출해야하는 각 컴포넌트마다 호출만 하면 편하게 사용할 수 있다.
Container은 상태와 로직들을 관리하고, Components 는 단순히 이 Container에서 props로 내려온 값을 받아 UI를 보여주는 역할을 한다.
import React, { useState, useEffect } from 'react';
import TodoList from './TodoList';
function TodoContainer() {
const [todos, setTodos] = useState([]);
useEffect(() => {
fetch('/api/todos')
.then((response) => response.json())
.then((data) => setTodos(data));
}, []);
return <TodoList todos={todos} />;
}
여기서 TotoContainer()에서 state나 effect를 선언하는 로직을 관리하며, TodoList()에서 props로 받아 단순 UI만 보여주는 역할을 한다.
HOC는 컴포넌트 자체를 매개변수로 받아 새로운 컴포넌트를 반환하는 패턴이다.
주로 공통된 로직을 여러 컴포넌트에서 재사용하기 위해 사용한다.
import React, { useEffect } from 'react';
function withLogging(WrappedComponent) {
return function(props) {
useEffect(() => {
console.log('Component Mounted');
return () => {
console.log('Component Unmounted');
};
}, []);
return <WrappedComponent {...props} />;
};
}
function MyComponent() {
return <div>Hello, world!</div>;
}
const MyComponentWithLogging = withLogging(MyComponent);
export default MyComponentWithLogging;
MyComponent() 가 컴포넌트이며, withLogging은 로직이다.
이 로직 함수를 따로 만들어 컴포넌트를 매개변수로 받아 새로운 컴포넌트로 만드는 것이다.
Render Props 패턴은 컴포넌트의 자식을 함수로 받아서, 그 함수에 데이터를 전달하고 그 데이터를 렌더링하는 방식이다.
import React, { useState, useEffect } from 'react';
function DataProvider({ render }) {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => setData(data));
}, []);
return data ? render(data) : <div>Loading...</div>;
}
function MyComponent() {
return (
<DataProvider
render={data => (
<div>
<h1>Data from API:</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)}
/>
);
}
export default MyComponent;
MyComponent() 컴포넌트에서 render 라는 props로 DataProvier() 컴포넌트를 호출한다.
render props 자체도 jsx로 이루어진 컴포넌트이다.
많은 곳에서는 Custom Hooks, Component Container 등등 이런 것들을 아키텍처 패턴이라고 구분하기도 디자인 패턴으로 구분하기도 한다.
사실 정답은 없지만, 필자는 디자인 패턴으로 생각하고 있다.
위의 패턴들은 훅과 컴포넌트를 단순히 분리하는 법, 훅을 다른 폴더에 나누어 관리하기 쉽도록 하는 법, 이런 이유로 사용되기에 전체적인 프로젝트 구조를 설명하는 데는 부족하다고 생각한다.
이제부터 아키텍처 패턴들을 알아보자.
애플리케이션을 여러 계층으로 나누어 의존성을 관리하고, 비즈니스 로직을 UI와 분리하는 방식이다.
너무나도 많이 참조했던 그 사진..
바깥 파란영역은 Web, UI, DB 등이 될 수 있으며 보통 Frameworks & Drivers라고 칭한다.
다양한 환경에서 제일 유저와 가까운 끝단의 영역이다.
중간 초록영역은 Controllers, Presenters, Gateways 등이 될 수 있으며 보통 Interface Adapters라고 칭한다.
유저와 유스케이스(버튼을 클릭한다, DB에 쿼리를 날린다 등)을 연결해주는 영역이다.
중간 빨간영역은 Use Cases로 애플리케이션의 비즈니스 로직을 정의하며, 사용자가 시스템과 상호작용하는 방법을 표현한다.
안쪽 노란영역은 Entities로 비즈니스 규칙과 데이터 모델을 정의하며, 애플리케이션의 핵심 부분이다.
특징
계층화: UI, 비즈니스 로직, 데이터 접근을 분리하여 각 계층의 독립성을 유지.
테스트 용이성: 비즈니스 로직을 독립적으로 테스트할 수 있어 유지보수가 용이.
/src
/entities
/User.js
/useCases
/UserService.js
/interfaces
/controllers
/UserController.js
/presenters
/UserPresenter.js
/frameworks
/api
/UserAPI.js
/components
/UserProfile.js
/app
/index.js
각각 매핑되는데 app은 뭐지? 라고 생각할 수 있다.
app은 index.js로 리액트에서 라우팅이나 redux를 쓴다면 store같은 것들이, 또 <React.StrictMode> 이런게 선언되는 그런 파일이라고 보면된다.
Flux는 Facebook에서 개발한 애플리케이션 아키텍처로, 단방향 데이터 흐름을 강조한다.
상태 관리를 중앙 집중화하여 애플리케이션의 상태를 예측 가능하게 만든다.
/src
/actions
/UserActions.js
/dispatcher
/AppDispatcher.js
/stores
/UserStore.js
/views
/UserProfile.js
Actions: 사용자의 동작을 설명하는 객체를 생성한다.
예를 들어, API 호출이나 사용자 입력에 대한 요청을 정의한다.
Dispatcher: 모든 액션을 처리하고, 이를 적절한 스토어에 전달하는 역할을 한다.
중앙 집중화된 데이터 흐름을 관리한다.
Stores: 애플리케이션의 상태를 보관하고, 비즈니스 로직을 처리한다.
스토어는 Dispatcher로부터 액션을 받아 상태를 업데이트한다.
Views: UI를 구성하는 컴포넌트로, 스토어의 상태를 기반으로 렌더링한다.
사용자의 입력을 받아 액션을 트리거한다.
Redux는 Flux의 변형으로 Reduce 의 단어와 Flux를 합친 것이다.
보통 상태관리 라이브러리 Redux에서 이 아키텍처를 사용한다.
/src
/actions
/reducers
/store
/components
Actions: 상태 변경을 설명하는 객체를 반환하며, 리듀서에 전달된다.
Reducers: 액션에 따라 상태를 변경하는 순수 함수로, 현재 상태와 액션을 인자로 받아 새로운 상태를 반환한다.
Store: 애플리케이션의 전체 상태를 보관하는 객체로, 상태의 구독 및 업데이트를 관리한다.
Components: Redux 스토어의 상태를 구독하고, 필요한 데이터를 UI에 렌더링한다.
Feature Sliced Design 으로 FSD 이분 글이 잘 설명되어 있다.
뭐 작은 프로젝트든 큰 프로젝트든 어디서든 적용되는 만능 아키텍처라고 하시는데, 본인이 판단해서 공부하기 바란다.
난 아직 어려워서 참조만 걸어서.. ㅜㅜ