모바일/Javascript

[AngularJS] MVC Pattern in Angular

박종명 2016. 7. 10. 11:41
728x90

MVC(Model-View-Controller)는 아주 오래된 SW아키텍처패턴이다.
(이 패턴이 소개된 시기는 1979년으로, 길지 SW공학분야에서는 고전적인 패턴이라 할 수 있다.)

이 패턴은 백엔드개발자(웹서버 사이드 개발자)에게는 이미 익숙한 패턴일 것이다.

자바의 JSP(스트럿츠), 서블릿, Bean 등의 상호연동 구조가 MVC 모델에 기반하고 있으며, 닷넷은 자사 웹개발플랫폼 기술 이름 자체를 ASP.NET MVC로 하여 MVC 구조를 지향하고 있다.

그러니, 그들(웹서버 사이드 개발자)이 이 패턴에 익숙한 것은 당연한 것이다.

반면, 웹 프론트엔드 개발자에게는 조금 낯설수도 있다.
물론 자바스크립트를 코어하게 학습하거나, ExtJS/Sench Touch와 같은 MVC 모델에 기반한 프레임워크를 다뤄본 적이 있다면, 역시 익숙한 개념일 것이다. (그래.. 중요한 것은 익숙함의 차이인 것이다.)

이 패턴의 핵심은, 모델이 어쩌구.. 뷰가 어쩌구가 아니다. 바로 기저에 깔린 사상이다.
그 사상이 바로 '관심사의 분리(관점의 분리)를 통한 상호 독립성 보장'인 것이다.

웹 프론트개발자 관점에서도 '관심사의 분리'라는 사상 자체는 너무나도 자연스러운 개념일 것이다.
바로 HTML과 CSS 그리고 Javascript의 역할 분리를 빗대어 보면 알 수 있다.

흔히 이 셋의 역할을 'HTML(구조)-CSS(표현)-Javascript(동작)'로 정의한다. 이것이 바로 역할의분리, 다시말해 관심사의 분리이며 이렇게 분리된 구조를 통해 상호 독립적인 개발이 보장되어 생산성과 유지보수성 등이 좋아지게 되는 것이다.

MVC 모델 역시 이와같은 관심사의 분리라는 SW공학적 원리가 적용된 아키텍처 패턴인 것이다.

* MVC 패턴
- 대부분의 응용프로그램은 데이터가 사용되며 이 데이터를 기반으로 사용자 인터페이스를 제공한다. MVC패턴은 데이터를 조작하는 비즈니스 로직과 사용자인터페이스를 서로 분리하여 상호 영향 없도록 하는 것에 초점을 맞춘 패턴이다. 이 둘(데이터와 인터페이스)의 상호연동을 위해 컨트롤러라는 새로운 계층을 두고 있는 것이다.

- Model: 응용프로그램의 핵심 로직과 데이터를 캡슐화 수행
- View: 사용자가 보게 되는 인터페이스의 총칭. 모델의 정보를 표시하는 역할수행
- Controller: 모델과 뷰의 연결을 위한 계층으로 사용자 입력을 받아 중계 수행

MVC 패턴에 대한 이론적 내용은 많은 인터넷 자료에서 쉽게 찾아 볼 수 있으니 여기서는 그만 다루도록 한다. 혹여 MVC라는 용어가 어색한 웹 프론트 개발자들이 반드시 짚고 넘어가야 할 점은, MVC는 관점의 분리라는 공학적 원리가 응용프로그램의 구조와 상호작용 부분에 적용된 모범사례라는 것 뿐이다.

* Server Side MVC? Client Side MVC?
- 웹개발 측면에서 보면, MVC 패턴은 웹 서버 사이드 개발에서의 응용프로그램의 구조 패턴으로 먼저 보편화되었다고 할 수 있다. 즉 JSP나 ASP.NET과 같은 서버 측 응용구조로 채택되어 지금까지도 많이 활용되고 있는 패턴이다.

그러나 오늘날 진보적인 웹 클라이언트 개발에 훌륭한 자바스크립트 라이브러리들이 등장하고 이 들이 추구하는 아키텍처 기반 구조 역시 MVC에 근거하고 있다.

전통적인 서버측 개발에서의 View의 영역은 (최종 랜더링되는) HTML 페이지이다. 즉 Controller와 Model은 서버측에 존재하며 (서버가 사용자에게 반환하는) HTML페이지가 바로 View의 영역이 되는 것이다.

이 말은 웹 클라이언트에 존재하는 웹 클라이언트 자체가 View가 된다는 것인데, 웹 클라이언트 영역에서도 이 부분을 다시 MVC로 나누고 있는 것이다. 뭔가 혼란스럽지 않은가?

웹 응용프로그램이란 것이 결국 서버측과 클라이언트 측이 결합되어 하나의 결과물이 되는 것인데, 각각의 영역에서 MVC 패턴을 따로 다룬다면, 전체의 MVC모습은 어떻게 된다는 말인가?

이것은 어떻게 보면 중요한 문제가 될 수 있다. 분업화된 작업방식(서버개발자/클라이언트 개발자)에서 각기 나름의 MVC 모델을 추구한다면 응용프로그램의 전체 구조는 가히 우스운 꼴이 될 수도 있다.

오히려 이 부분은 웹 프론트앤드 개발자에게는 익숙한 것일지도 모른다. 그것은 웹 응용프로그램의 구현모델의 차이를 이해하면 되는 것이다.

* SPA(Single Page Application)에서의 MVC
- 서버측 MVC, 클라이언 측 MVC라는 말 자체가 조금 억지스럽다. 그것은 응용프로그램의 구현모델상의 차이가 날 수 있는 부분이지, 서버와 클라이언트로 구분되지는 않을 것이다.

AngularJS는 SPA 모델을 위한 자바스크립트 프레임워크이다.
SPA는 전통적인 웹 구조와 상반되는 개념으로, 기존의 웹 페이지는 데이터를 전송하고 서버가 이를 처리한 후 완전히 새로운 페이지를 랜더링하는 구조였다.(이를 '호출 후 새로고침' 방식이라 한다)

그러나 현대의 웹 페이지는 단순히 정적 페이지라는 개념을 넘어 사용자의 이벤트에 반응하고 적절한 상호작용을 제공하며 애니메이션과 네비게이션을 자체적으로 해결하는 응용프로그램의 면모를 보여줄 수 있다. 그리고 원격지 서버와의 통신은 더이상 완전히 새로운 페이지의 '새로고침'이 아니라 단일 페이지에서 필요한 데이터만 비동기로 가져와서 사용자의 작업을 방해하지 않으면서 데이터를 갱신시킨다.

결론적으로 전통적인 새로고침 방식의 웹과 SPA 방식의 웹에서의 MVC의 역할 모델이 조금 상이할 수 있다고 할 수 있다.

다시 말하지만, AngularJS는 SPA 실현을 가능케 하는 자바스크립트 프레임워크이다.
SPA모델에서는 대부분의 작업이 클라이언트 측에서 이루어진다. 그리고 서버측 데이터 연동은 Ajax와 같은 통신 모델을 이용하여 비동기로 그때그때 필요한 데이터만 가져와서 사용자 화면을 갱신시킨다. 이런 모델에서의 MVC패턴의 각 영역은 다음과 같다.

- Model: View가 사용할 데이터 또는 공통되고 주요한 비즈니스 로직을 정의한 영역으로 실제 데이터는 대체로 서버측에 존재하게 된다.
- View: 사용자 화면을 구성하는 HTML 영역
- Controller: 뷰와 모델을 연결해 주는 영역으로 SPA모델에서는 클라이언트 측에 존재하게 된다. 좀 더 면밀히 따져보자면, SPA 모델에서는 Controller의 영역을 ViewModel(뷰모델)로 설계되기도 하며, 이는 SPA뿐만 아니라 전통적인 윈도우 응용프로그램(요청과 응답 구조의 웹구조가 아닌) 환경에서 뷰와 모델간 의존성을 제거하고 뷰의 상태와 뷰에 투영될 데이터의 변화에 따른 자동 바인딩을 가능케 하는 구조로써 이와 같은 모델을 MVC의 변형된 모델로 MVVM 패턴이라고 불리운다.

큰 틀에서 정리하자면, Model은 그 역할이, 응용프로그램 전체에 걸쳐 필요한 데이터와 비즈니스 로직을 제공하게 되는데, SPA 구조에서는 이 부분을 클라이언트 측에 추상화 하여 일종의 공용서비스 형태로 구현하며, 실제 데이터는 서버로부터 비동기 형태로 (백그라운드에서) 받아와서 추상화된 서비스계층에 할당하고 뷰모델이라는 중간계층을 경유해 뷰와 바인딩시키는 구조인 것이다.

결국 SPA 에서의 서버측은 응용프로그램에 필요한 데이터를 비동기로 서비스 해 주는 기능만 존재하면 된다는 말이다.(이부분은 대체로 REST API로 구현된다)

SPA모델에서의 MVC는 기존의 새로고침 모델에서의 MVC와는 그 역할에 따른 컴포넌트의 위치와 통신구조에서 차이점을 보이며, 어떤 형태의 모델로 웹 응용프로그램을 구현하는지에 따라 '모델-뷰-컨트롤러'의 실질적 구현레벨의 모습이 약간 다를 수 있다는 것이다.

하지만 관점의 분리라는 사상과 비즈니스 로직(모델)과 뷰를 분리하는 아키텍처 구조는 SPA이든 새로고침방식이든 다르지 않다는 것은 원론적으로 중요하다.

* AngularJS의 MVC 컴포넌트
- AngularJS에서는 MVC 패턴을 기반으로 응용프로그램 구성을 가능케 한다. 어떤이는 MVVM이라 예기하고 싶겠지만 여기서는 상황을 일반화 시켜서 글을 이어 나가도록 한다. MVVM역시 MVC에서 필요에 의해 일부 변형시킨 패턴이며 이 글에서는 그 차이가 그리 중요하지 않기 때문이다.

(실제로 AngularJS는 자신의 아키텍처 기반을 MVW(Model-View-Whatever)라고 부르며 Whatever란 어떤것이라도 가능하다는 뜻으로, MVC/MVP/MVVM 모두 가능하다는 의미이다. 아키텍처 패턴간 발전과정과 특징 및 구조적 차이는 분명이 존재하며 이 들의 관계는 다른 글에서 다시 언급하도록 한다.)

MVC 아키텍처를 위해 AngualrJS가 제공하는 컴포넌트를 알아보자. 다음은 AngularJS 공식 사이트에서 설명하는 내용이다.

* Model — Models are the properties of a scope; scopes are attached to the DOM where scope properties are accessed through bindings.
* View — The template (HTML with data bindings) that is rendered into the View.
* Controller — The ngController directive specifies a Controller class; the class contains business logic behind the application to decorate the scope with functions and values

 AngularJS가 MVC 구현을 위해 제공하는 것중 핵심은, ngController 디렉티브이며 이를 이용하여 뷰를 위한 컨트롤러 클래스를 생성할 수 있다.

Model은 scope의 프로퍼티라고 설명하고 있다. AngularJS에서 $scope은 뷰와 컨트롤러의 연동을 위해 제공되는 객체이며 컨트롤러 객체에서 뷰로 데이터를 노출하기 위한 일반적인 방법을 제공한다.
scope이 모델이다라고 애매한 해석을 하기 보다는, 컨트롤러에서 모델 정보를 뷰로 연결하기 위해 제공되는 객체로 이해하면 될 것이다.

실제 예제를 한번 보자.

<div ng-app ng-init="qty=1;cost=2">
  <b>Invoice:</b>
  <div>
    Quantity: <input type="number" min="0" ng-model="qty">
  </div>
  <div>
    Costs: <input type="number" min="0" ng-model="cost">
  </div>
  <div>
    <b>Total:</b> {{qty * cost | currency}}
  </div>
</div>

코드는 뷰와 모델간 가장 기본적인 상호작용을 설명하기 위해 작성되었으며, 데이터에 해당하는 Model정의를 위해 ng-model 디렉티브를 사용했다. ng-model 디렉티브가 지정된 입력박스의 값은 자동으로 $scope객체의 프로퍼티로 정의되며, 더블중괄호로 표현되는 '{{ property of scope }}' 표현식을 통해 모델의 정보를 View로 연결키실 수 있다.

다음 이에 대한 설명으로 AngularJS 공식 사이트에서 제공하는 그림이다.

 

이제 컨트롤러를 등장시켜 보자.

angular.module('myModule', [])
.controller('InvoiceController', function() {
  this.qty = 1;
  this.cost = 2;
  this.total = function total(outCurr) {
    return this.qty * cost;
  };
  this.pay = function pay() {
    window.alert("Thanks!");
  };
});

<div ng-app="myModule" ng-controller="InvoiceController as invoice">
  <b>Invoice:</b>
  <div>
    Quantity: <input type="number" min="0" ng-model="invoice.qty" required >
  </div>
  <div>
    Costs: <input type="number" min="0" ng-model="invoice.cost" required >
  </div>
  <div>
    <b>Total:</b>
    <span>{{invoice.total}}</span>
    <button class="btn" ng-click="invoice.pay()">Pay</button>
  </div>
</div>

컨트롤러는 뷰가 바인딩하고 다룰 수 있는 속성과 메서드를 정의하기 위한 객체로써 AngularJS 모듈에 연결한다. 코드를 보면 $scope 객체를 찾아볼 수 없는데 이는 controller-as 문법을 사용하여 $scope객체를 명시적으로 사용하지 않아도 되기 때문이다. 중요한 것은 View와 Scope 그리고 Controller의 상호작요이며 다음 그림에 도식화하여 설명하고 있다.

 

* 일반적인 MVC 패턴 구현
- 이제 AngularJS에서 일반적 MVC 패턴 구현의 전체 모습을 살펴보자. Model의 경우 원격지 서버에 존재하는 데이터를 기반으로 하는게 일반적이며 이와같은 공통적인 데이터를 AngularJS 어플리케이션 전체에 공유하기 위해서는 AngularJS의 Service(서비스) 객체로 생성하는 것이 좋다.

사실 이전까지의 글보다 다음의 코드를 이해하고 이 코드를 기반으로 AngularJS MVC 패턴 구현을 이해하는 것이 보다 현실적이다.

먼저index.html을 다음과 같이 작성한다

<!DOCTYPE html>
<html>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script src="app.js"></script>
<body>
 <div ng-app="myModule" ng-controller="MainCtrl as main">
  <h1>Persons</h1>
  <ul>
     <li ng-repeat="person in main.persons">
        {{person.name + ', ' + person.city}}
     </li>
   </ul>
 </div>
</body>
</html>

AngularJS 관련 자바스크립트 파일을 app.js라는 별도의 파일로 분리했다.
그리고 AngularJS의 모듈과 컨트롤러를 매핑시킨다. 이때 $scope객체를 사용하는 대신, controller-as 문법을 사용한다. 그리고 배열로 반환되는 persons 정보를 ng-repeat 디렉티브로 순환시켜 <ul>태그의 <li>항목으로 각각 매핑시킨다.

그리고 다음은 app.js 파일의 코드이다.

//모듈 생성
var app = angular.module('myModule', []);
 
//컨트롤러 생성(컨트롤러 생성자에 PersonModel서비스 전달=>의존성주입(생성자 주입)패턴)
app.controller('MainCtrl', function(PersonModel){
 var main = this;
 //Personmodel서비스에 정의된 getPersons()함수를 호출
 main.persons = PersonModel.getPersons();
});

//서비스 생성
//서비스는 전체 어플리케이션에서 사용하게 될 데이터나 공통 기능을 작성하기 좋은 곳이다
//보통 MVC의 Model 정보를 원격지에서 불러오는 역할을 하도록 한다.
 app.service('PersonModel', function(){
   var service = this;

  //실제환경에서는 persons 정보는 원격지 서버로 부터 받아오는 것이 일반적이지만,
  //여기서는 코드의 단순화를 위해 직넙 객체 리터럴로 생성한다.
   var persons = [
       {name: 'Park', city: 'Pusan'},
       {name: 'KIM', city: 'Seoul'},
       {name: 'Jung', city: 'suwon'}
     ];
    
   service.getPersons = function(){
     return persons;
   };

   service.setPersons = function(){
     //서버로 부터 받아서 업데이트

   };
});

Model 정보를 제공하는 서비스 객체(PersonModel)를 생성하여 컨트롤러 생성자 함수에 주입시킨다. 이렇게 함으로써 Controller와 Model을 물리적으로 분리가능하며 의존성 주입패턴으로 이 둘을 의존성을 관리한다. 컨트롤러는 이제 PersonModel의 속성과 메서드를 사용할 수 있게 되었다.

참고로 서비스와 컨트롤러에서 this키워드를 변수에 저장시키는 것은, 자바스크립트의 동적언어 특징에서 기인하는 문맥(Context)상 this의 해석이 모호해 지는 점을 예방하기 위해 명시적으로 할당해 두는 것이다. 이는 예외적인 동작에 대한 예방차원에서 필요한 코드로 자바스크립트 특징에 기반한 코드사례이다.

결과 화면은 다음과 같다.

 

지금까지 AngularJS의 MVC 패턴 구현에 대해 장황하게(?) 알아보았다.

대부분의 AngularJS 응용프로그램의 코드 구조는 여기에 살을 더 붙여 나가는 식으로 작성하면 된다. MVC 패턴에 기반한 응용프로그램 구조는 영역간 분리를 통해 상호 독립적 개발과 유지, 단위테스트를 가능케 하여 전체적인 생산성과 유지보수성을 향상시킨다.

중요한 것은 MVC의 사상을 이해하고 AngularJS가 MVC를 위해 제공하는 컴포넌트를 적절히 조합하여 응용프로그램을 구성하는 것이다.

이 글을 바탕으로 좀더 세말한 치장을 해 나가면 될 것이다.