프론트엔드 아키텍처. 웹 애플리케이션의 구조적 설계를 의미한다. 여기서 구조적 설계란 코드의 구성, 파일 구조, 데이터 흐름 및 상호작용 방식 등을 포함하는 개념이다.
아키텍처의 사용은 개발자에게 일관된 코드 구조를 제공하고, 유지보수를 용이하게 하며, 팀원 간의 협업을 효율적으로 만들어준다. 또한 확장성, 재사용성, 테스트 용이성, 성능 최적화 등의 이점도 제공한다.
이를 위해서 프론트엔드 개발에는 다양한 아키텍처 '패턴'들이 사용되고 있다. 이들은 애플리케이션의 구조를 정의하고, 데이터 흐름, 상태 관리, UI 렌더링과 같은 핵심적인 문제들을 해결하는 데 도움을 주는 역할을 한다.
프론트엔드 아키텍처에는 모델-뷰-뷰모델(MVVM), 모델-뷰-컨트롤러(MVC), 플럭스(Flux), 리덕스(Redux) 등이 있다. 각각의 패턴은 특정 문제를 해결하기 위해 고안되었으며, 프로젝트의 요구사항과 개발 팀의 선호도에 따라 적합한 패턴을 선택할 수 있다.
모델(Model), 뷰(View), 뷰모델(ViewModel)의 세 가지 주요 구성 요소로 이루어진 아키텍처 패턴. 각각의 역할이 명확하게 분리되어 있어, 코드의 유지보수와 확장이 용이하다는 장점을 갖는다.
모델Model: 데이터와 비즈니스 로직을 담당한다.
뷰View: 사용자 인터페이스(UI)를 표시한다.
뷰모델ViewModel: 뷰와 모델 사이의 데이터 바인딩 및 상태 관리를 담당한다.
// 모델 (Model)
class UserModel {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
// 뷰모델 (ViewModel)
function useUserViewModel() {
const [user, setUser] = useState(new UserModel('', ''));
function updateUser(name, email) {
setUser(new UserModel(name, email));
}
return { user, updateUser };
}
// 뷰 (View)
function UserComponent() {
const { user, updateUser } = useUserViewModel();
return (
<div>
<input
value={user.name}
onChange={e => updateUser(e.target.value, user.email)}
/>
<input
value={user.email}
onChange={e => updateUser(user.name, e.target.value)}
/>
</div>
);
}
뷰 모델을 구현하는 방법은 커스텀 훅을 사용하는 것과, 클래스를 사용하는 방법이 존재한다. 각각의 장단점은 아래와 같다.
재사용성: 비슷한 기능을 하는 컴포넌트 간에 상태 로직을 쉽게 재사용할 수 있다.
컴포지션: 다양한 훅을 조합하여 복잡한 기능을 구현할 수 있어. 이는 코드 재사용성과 가독성을 높여준다.
간결성과 명료성: 함수 기반으로, 클래스보다 간결하고 이해하기 쉬운 코드를 작성할 수 있다.
React 기능과의 호환성: useState, useEffect 같은 React 내장 훅과 자연스럽게 통합되어 사용될 수 있다.
인스턴스 상태의 부재: 커스텀 훅은 인스턴스를 가지지 않기 때문에, 특정 인스턴스의 상태를 유지하기 어렵다.
테스트의 복잡성: 종종 훅이 컴포넌트와 밀접하게 연결되어 있어서, 독립적인 테스트가 어려울 수 있다.
명시적인 인스턴스 관리: 클래스는 인스턴스를 가지며, 인스턴스 변수와 메소드를 사용하여 상태를 관리할 수 있다.
생명주기 메소드: componentDidMount, componentDidUpdate 등의 생명주기 메소드를 사용하여 복잡한 상태 관리 및 사이드 이펙트를 처리할 수 있다.
테스트 용이성: 클래스 컴포넌트는 인스턴스화할 수 있기 때문에, 독립적으로 테스트하기 쉽다.
보일러플레이트 코드: 클래스는 커스텀 훅에 비해 더 많은 보일러플레이트 코드를 작성해야 해. 이로 인해 코드가 길어지고 복잡해질 수 있다.
재사용성의 한계: 클래스 내의 메소드와 상태는 다른 클래스로 쉽게 재사용하기 어렵다.
학습 곡선: 클래스 컴포넌트는 함수 컴포넌트보다 복잡한 개념과 생명주기 메소드를 가지고 있어, 학습하기가 더 어려울 수 있다.
MVC는 모델(Model), 뷰(View), 컨트롤러(Controller)로 구성된다. MVC는 주로 서버 사이드에서 사용되지만, 클라이언트 사이드에서도 적용할 수 있다.
모델Model: 데이터와 비즈니스 로직을 처리한다.
뷰View: 사용자에게 보여지는 UI 부분을 담당한다.
컨트롤러Controller: 사용자의 입력을 처리하고 모델과 뷰를 연결한다.
// 모델 (Model)
class UserModel {
constructor(name, email) {
this.name = name;
this.email = email;
}
// 데이터베이스 연동 로직 추가 가능
}
// 뷰 (View) - EJS, Pug 등의 템플릿 엔진 사용
// 컨트롤러 (Controller)
class UserController {
async createUser(req, res) {
const { name, email } = req.body;
const user = new UserModel(name, email);
// 데이터베이스에 저장하는 로직 추가
res.render('userView', { user });
}
}
MVVM과 MVC의 차이점은 ViewModel와 Controller의 존재에서 발생된다.
데이터 바인딩: MVVM은 데이터 바인딩을 사용하여 뷰모델과 뷰 사이의 동기화를 자동화한다. MVC에서는 이런 데이터 바인딩이 없으며, 컨트롤러를 통해 뷰와 모델 사이의 상호작용이 이루어진다.
역할 분담: MVVM에서는 뷰모델이 사용자의 입력 처리와 데이터 표현의 책임을 공유하지만, MVC에서는 컨트롤러가 사용자 입력 처리를, 뷰가 데이터 표현을 전담한다.
복잡성: 일반적으로 MVVM은 데이터 바인딩으로 인해 더 복잡한 로직을 간결하게 처리할 수 있다. 반면, MVC는 더 전통적이고 직관적인 흐름을 가지고 있어서 이해하기 쉽다.
이러한 차이점들 때문에, MVVM은 주로 데이터 바인딩을 지원하는 프레임워크 (예: Angular, Vue.js, React와 MobX 또는 Redux와 함께 사용)에서 선호되며, MVC는 서버 사이드 애플리케이션 (예: Ruby on Rails, Django)에서 더 일반적으로 사용된다.
플럭스는 React에서 사용하기 위해 Facebook에 의해 개발된 아키텍처 패턴이다. 플럭스의 핵심은 단방향 데이터 흐름이다. 이는 애플리케이션의 상태 관리를 더 예측 가능하고 이해하기 쉽게 만든다.
디스패처Dispatcher: 애플리케이션에서 데이터 흐름을 관리한다.
스토어Store: 애플리케이션의 상태와 로직을 담당한다.
뷰View: 사용자 인터페이스를 표시하고 사용자의 상호작용에 반응한다.
// 액션 (Action)
function createUserAction(name, email) {
return {
type: 'CREATE_USER',
payload: { name, email }
};
}
// 스토어 (Store)
class UserStore {
constructor() {
this.state = { users: [] };
// 스토어 초기화 및 디스패처 등록
}
// 스토어 업데이트 로직
}
// 뷰 (View)
function UserComponent() {
// 스토어 상태 사용 및 액션 디스패치
}
리덕스는 플럭스 아키텍처에 영감을 받아 만들어진 상태 관리 라이브러리다. 리덕스는 애플리케이션의 상태를 전역적으로 관리하며, 상태의 일관성과 예측 가능성을 제공한다.
액션Action: 상태를 변경하는 이벤트를 정의한다.
리듀서Reducer: 액션에 따라 상태를 어떻게 변경할지 정의한다.
스토어Store: 애플리케이션의 상태를 저장한다.
// 액션 (Action)
const CREATE_USER = 'CREATE_USER';
// 액션 생성자 (Action Creator)
function createUser(name, email) {
return {
type: CREATE_USER,
payload: { name, email }
};
}
// 리듀서 (Reducer)
function userReducer(state = [], action) {
switch (action.type) {
case CREATE_USER:
return [...state, action.payload];
default:
return state;
}
}
// 스토어 (Store)
const store = createStore(userReducer);
리덕스가 아키텍처?
나는 지금까지 리덕스를 그냥 중앙 상태관리 라이브러리로만 알고 사용하고 있었다. 그래서 리덕스가 프론트엔드 아키텍처 중 하나라는 내용을 봤을 때, 이게 무슨 소리인가 이해를 못하고 있었는데..
리덕스를 사용하기 위해 구현했던 스토어, 리듀서, 액션 등의 구조와 동작 원리 그 자체가 프론트엔드 아키텍처의 한 종류였다는 것을 뒤늦게 깨달았다!
대중적으로 사용되고 있는 패턴은 이렇지만 IT 분야가 원래 빠르게 변화하는 곳인지라, 이쪽 개념도 항상 변화하고 있다.
최신 웹 개발 트렌드에는 마이크로 프론트엔드, 서버리스 아키텍처, 프로그레시브 웹 앱(PWA) 등이 있다고 한다.
큰 프론트엔드 애플리케이션을 더 작고, 독립적이며, 쉽게 관리할 수 있는 여러 개의 작은 애플리케이션으로 나누는 개발 접근 방식.
자신의 애플리케이션 부분에 대해 더 높은 자율성을 가질 수 있고, 기술 스택의 다양성이 허용됩니다. 또한, 더 작은 코드 베이스는 유지보수와 빠른 개발을 가능하게 한다.
서버리스 아키텍처는 애플리케이션을 개발하고 실행할 때 서버 관리를 최소화하는 클라우드 컴퓨팅 실행 모델입니다. 개발자는 코드 작성에 집중할 수 있으며, 서버 관리는 클라우드 제공 업체가 담당한다.
확장성이 뛰어나며, 사용한 만큼의 비용만 지불하는 비용 효율성이 있습니다. 또한, 인프라 관리에 대한 부담이 줄어든다.
네이티브 앱의 장점을 결합한 애플리케이션입니다. 웹 기술을 사용하여 만들어지며, 네이티브 앱과 유사한 사용자 경험을 제공한다.
설치 없이 액세스 가능하고, 오프라인 작동, 푸시 알림, 빠른 로딩과 같은 네이티브 앱의 기능을 지원합니다. 또한, 다른 장치와의 호환성이 높다.