-
22장 Angular.js(앵귤러.js) MVC 모듈 만들기 완성MeanStack (deprecated) 2016. 7. 25. 23:04
이번엔 저번장에 이어서 Angular.js 로 MVC 모듈을 만들어 보겠다.
이 모듈은 $resource 팩토리를 사용해 익스프레스 API와 통신하는 Angular.js 서비스, 클라이언트 쪽 모듈논리를
포함하는 Angular.js 컨트롤러, CRUD 연산을 수행하는 인터페이스를 사용자에게 제공하는 뷰 집합을 포함한다.
먼저 모듈의 초기 구조를 잡아보자. static/ 폴더로 가서 articles 이라는 폴더를 새로 생성한다.
이 새 폴더에 articles.client.module.js 라는 모듈 초기화 파일을 생성하고 다음 코드를 넣자.
angular.module('articles',[]);
위 코드는 모듈 초기화를 다루지만, 주 애플리케이션 모듈의 의존성으로 새로운 모듈을 추가 할 필요가 있다.
이를 위해, 다음 static/ 폴더로 가서 application.js 파일을 변경하자.
var mainApplicationModuleName = 'mean'; var mainApplicationModule = angular.module(mainApplicationModuleName, ['ngResource','ngRoute','users','example','articles']); // articles 추가 mainApplicationModule.config(['$locationProvider', function($locationProvider) { $locationProvider.hashPrefix('!'); } ]); if(window.location.hash === '#_=_') window.location.hash = '#!'; angular.element(document).ready(function(){ angular.bootstrap(document, [mainApplicationModuleName]); });
위 코드에서 articles 라는 새로운 모듈을 추가 하였다.
CRUD 모듈이 API 종단점과 쉽게 통신하기 위해 $resource 팩토리 메소드를 활용하는 단일 Angular.js 서비스
사용을 권장한다. 이를 위해 static/articles 폴더로 가서 services 라는 폴더를 생성하자.
그리고 services 폴더에 articles.client.service.js 라는 파일을 만들고 다음 코드를 넣자.
angular.module('articles').factory('Articles',['$resource', function($resource){ return $resource('api/articles/:articleId',{ articleId : '@_id' }, { update : { method : 'PUT' } }); }]);
위 코드를 보면 서비스가 $resource 팩토리를 사용하는 방식에 주목하자. 넘기는 세 인수는 자원 종단점을 위한
기초 URL, 글의 다큐먼트 _id 필드를 사용하는 라우팅 매개변수, PUT HTTP 메소드를 사용하는 update() 메소드로
자원 메소드를 확장하는 동작 인수다.
이젠 Angular.js 모듈 컨트롤러를 만들 차례다. 알다시피, 모듈 논리의 대다수는 일반적으로 Angular.js 컨트롤러로
구현된다. 여기서는 컨트롤러가 CRUD 연산을 수행하기 위해 필요한 모든 메소드를 제공할 수 있어야 마땅하다.
먼저 컨트롤러 파일 생성 작업부터 시작하자. 이를 위해 static/articles 폴더로 가서 controllers 라는 새로운 폴더를
생성하자. 그리고 이 폴더에 articles.client.controller.js 라는 파일을 생성하고 다음 코드를 넣자.
angular.module('articles').controller('ArticlesController',['$scope', '$routeParams','$location','Authentication','Articles', function($scope,$routeParams,$location,Authentication,Articles) { $scope.authentication = Authentication; } ]);
새롭게 만든 ArticlesController 컨트롤러가 주입된 4가지 서비스를 사용하는 방식에 주목하자.
각 서비스는
- $routeParams : ngRoute 모듈이 제공하며, 다음에 정의할 AngularJS 라우터의 라우터 매개변수에 대한
참조를 담고 있다.
- $location : 애플리케이션의 탐색을 제어한다.
- $Authentication : 예전에 만든 인증된 사용자 정보를 제공하는 서비스 이다.
- Articles : 앞에서 만든 서비스이며, RESTful 종단점과 통신할 수 있는 메소드 집합을 제공한다.
컨트롤러가 Authentication 서비스를 $scope 객체와 결합해 뷰에서도 이를 사용 가능하게 만드는 방식을
주목하자. 컨트롤러를 정의 했으므로 , 이제 컨트롤러 CRUD 를 구현하는 것 은 쉬울 것 이다.
먼저 글 생성을 위한 create() 메소드를 만들어보자. create() 메소드는 이 메소드를 호출한 뷰에서 title 과
content 폼 필드를 사용하고 , Articles 서비스를 사용해 대응하는 RESTful 종단점과 통신하며 새로운 도큐먼트를
저장한다. create() 메소드를 구현하기 위해 , static/articles/controllers 폴더로 가서 articles.client.controller.js
파일에 다음 코드를 추가하자.
$scope.create = function() { var article = new Articles({ title : this.title, content : this.content }); article.$save(function(response) { $location.path('articles/' + response._id); }, function(errorResponse) { $scope.error = errorResponse.data.message; }); };
create() 메소드의 기능을 살펴보자. 먼저 , title 과 content 폼 필드와 Articles 자원 서비스를 사용해 새로운 글 자원
을 생성했다. 그리고 나서, 글 자원의 $save() 메소드를 사용해 새로운 articles 객체를 콜백 두 개와 함께 대응하는
RESTful 종단점으로 전송한다. 첫번째 콜백은 성공적인 HTTP 요청을 표시하는 성공(200) 상태 코드로 서버가 응답
할 때 실행될 것이다. 그러면 콜백은 $location 서비스를 사용해 생성된 글을 표현할 라우터를 탐색한다.
둘째 콜백은 실패한 HTTP 요청을 표시하는 오류 상태 코드로 서버가 응답할 때 실행될 것이다. 그러면 콜백은
오류 메시지를 scope 객체에 대입해 뷰가 사용자에게 오류를 표현할 수 있게 만든다.
이번엔 find() 와 findOne() 메소드를 만들어보자. 이는 알다시피 글의 전체리스트와 글 하나를 읽는 메소드다.
이를 구현하기 위해 static/articles/controllers/ 폴더로 가서 articles.client.controller.js 파일을 열어 다음 코드를
추가하자.
$scope.find = function() { $scope.articles = Articles.query(); }; $scope.findOne = function() { $scope.article = Articles.get({ articleId : $routeParams.articleId }); };
코드를 살펴보면 findOne() 메소드는 이 함수가 URL에서 직접 얻은 articleId 라우트 매개변수에 기반한 단일 글을
인출 할 것이다. find() 메소드는 전체 리스트를 출력하기 위해 자원의 query() 메소드를 사용하는 반면, findOne()
메소드는 단일 다큐먼트를 인출하기 위해 자원의 get() 메소드를 사용한다. 두 메소드가 결과를 $scope 변수에
대입해 뷰가 데이터를 표현할 수 있게 만드는 방식에 주목하자.
이번엔 글을 수정하기 위한 update() 메소드를 만들어 보자. update() 메소드는 $scope.article 변수를 사용하여 ,
뷰 입력 내용으로 글을 갱신하고, 대응하는 RESTful 종단점과 통신하기 위해 위 다른 메소드와 마찬가지로 Articles
서비스를 사용하며, 걍신된 다큐먼트를 저장한다. static/articles/controllers 폴더로 가서 articles.client.controller.js
파일에 다음 코드를 붙여 넣자.
$scope.update = function() { $scope.article.$update(function(){ $location.path('articles/' + $scope.article._id); }, function(errorResponse){ $scope.error = errorResponse.data.message; }); };
update() 메소드를 살펴보면 글 자원의 $update() 메소드를 사용해 갱신된 article 객체를 콜백 두 개와 함께 대응
하는 RESTful 종단점으로 전송한다. 첫번째 콜백은 성공적인 HTTP 요청을 표시하는 성공(200) 상태 코드로 서버가
응답할 때 실행될 것 이다. 그러면 콜백은 $location 서비스를 사용해 갱신된 글을 표현할 라우터를 탐색한다.
두번째 콜백은 실패한 HTTP 요청을 표시하는 오류 상태 코드로 서버가 응답할 때 실행 될 것이다. 그러면 콜백은
오류 메시지를 $scope 객체에 대입해 뷰가 사용자에게 오류를 표현 할 수 있게 만든다. (응?)
create() 메소드와 동작방식이 똑같다고 보면 된다.
자 이제 마지막 delete() 메소드를 만들어 보자. 사용자는 read 뷰는 물론이고 list 뷰에서 글을 지울지도 모르기에,
delete() 메소드는 $scope.article 또는 $scope.articles 변수를 사용할 것 이다.
위와 같이 static/articles/controllers 폴더로 가서 articles.client.controller.js 파일에 다음 코드를 붙여 넣자.
$scope.delete = function(article) { if (article) { article.$remove(function() { for (var i in $scope.articles) { if($scope.articles[i] === article) { $scope.articles.splice(i, 1); } } }); } else { $scope.article.$remove(function() { $location.path('articles'); }); } };
delete() 메소드는 사용자가 목록에서 글을 지우는지 article 뷰에서 글을 바로 지우는지를 파악할 것 이다.
그리고 나서, 글의 $remove() 메소드를 사용해 대응하는 RESTful 종단점을 호출 할 것 이다. 사용자가 목록에서
글을 지웠다면, 삭제된 객체 또한 글 컬렉션에서도 제거해야 한다. 그렇지 않으면 글이 삭제된 다음 리스트 뷰에서
다시 등장하게 될 것 이다.
이제 컨트롤러 설정을 다 마쳤으므로 , Angular.js 뷰를 구현하고 라우팅 메커니즘에 연결하는 단계로 넘어가자.
자 먼저 글을 생성할 뷰를 만들어 보자. static/articles 폴더로 가서 views 라는 폴더를 새로 만들자. 그리고
static/articles/views 폴더에서 create-article.client.view.html 이라는 파일을 생성하고 새로운 파일에 다음
코드를 넣자.
<section data-ng-controller="ArticlesController"> <h1>New Article</h1> <form data-ng-submit="create()" novalidate> <div> <label for="title">Title</label> <div> <input type="text" data-ng-model="title" id="title" placeholder="Title" required> </div> </div> <div> <label for="content">Content</label> <div> <textarea data-ng-model="content" id="content" cols="30" rows="10" placeholder="Content"></textarea> </div> </div> <div> <input type="submit"> </div> <div data-ng-show="error"> <strong data-ng-bind="error"></strong> </div> </form> </section>
create-article 뷰는 텍스트 입력 창 2개와 제출 버튼으로 구성된 단순 폼을 포함한다.텍스트 입력창은 ng-model
지시자를 사용해 사용자 입력을 컨트롤러 영역에 결합하고, ng-controller 지시자에 대한 명세로, 이 컨트롤러는
ArticlesController가 될 것이다. 또한 ng-submit 지시자를 form 엘리먼트에 지정한 사실에도 주목할 필요가 있다.
이 지시자는 Angular.js 에게 폼을 제출할 때 특정 컨트롤러 메소드를 호출하게 알려준다. 여기서는 폼을 제출할
경우 컨트롤러의 create() 메소드를 수행 할 것 이다. 마지막으로 폼의 끝 부분에 있는 오류 메시지로, 생성 오류가
발생 할 경우에 보여질 것이다.
이제 글을 보는 뷰를 생성해보자. view-article 뷰는 이미 존재하는 글을 보기 위한 것 으로 이미 존재하는 글을
얻기 위해 findOne() 메소드를 사용한다. 그리고 작성자가 글을 삭제하거나 update-article 뷰를 탐색하게 만들
기 위해 오직 직접 글을 쓴 작정자에게만 보이는 버튼 집합도 포함한다.
뷰를 생성하기 위해 static/articles/views 폴더로 가서, view-article.client.view.html 이라는 새로운 파일을 생성
하고 코드를 넣자.
<section data-ng-controller="ArticlesController" data-ng-init="findOne()"> <h1 data-ng-bind="article.title"></h1> <div data-ng-show="authentication.user._id == article.creator._id"> <a href="/#!/articles/{{article._id}}/edit">edit</a> <a href="#" data-ng-click="delete();">delete</a> </div> <small> <em>Posted on</em> <em data-ng-bind="article.created | date : 'medium'"></em> <em>by</em> <em data-ng-bind="article.creator.username"></em> </small> <p data-ng-bind="article.content"></p> </section>
View-article 뷰는 ng-bind 지시자를 사용해 글 정보를 표현하는 단순한 HTML 엘리먼트 집합을 포함한다.
create-article 뷰에서 수행한 작업과 유사하게, ng-controller 지시자를 사용해 뷰에게 ArticlesController 를
사용한다고 알려준다. 하지만 글 정보를 읽을 필요가 있기에, 뷰는 ng-init 지시자를 사용해 뷰가 올라올 때
컨트롤러의 findOne() 메소드를 호출한다. 또한 작성자에게 글 편집과 삭제 링크를 보여주기 위해 ng-show
지시자를 사용하는 방식을 주목하자. 첫째 링크는 사용자가 update-article 뷰로 가게 만드는 반면, 둘째 링크는
컨트롤러의 delete() 메소드를 호출 할 것이다.
자 이번엔 edit-article 뷰를 만들어보자. edit-article 뷰는 이미 존재하는 글을 갱신하기 위한 뷰이다. 이 뷰는
update() 메소드를 사용해 갱신된 글을 저장한다. static/articles/views 폴더로 가서 edit-article.client.view.html
파일을 생성하고 코드를 붙여 넣자.
<section data-ng-controller="ArticlesController" data-ng-init="findOne()"> <h1>Edit Article</h1> <form data-ng-submit="update()" novalidate> <div> <label for="title">Title</label> <div> <input type="text" data-ng-model="article.title" id="title" placeholder="Title" required> </div> </div> <div> <label for="content">Content</label> <div> <textarea data-ng-model="article.content" id="content" cols="30" rows="10" placeholder="Content"></textarea> </div> </div> <div> <input type="submit" value="Update"> </div> <div data-ng-show="error"> <strong data-ng-bind="error"></strong> </div> </form> </section>
edit-article 뷰는 텍스트 입력창 두 개와 제출 버튼으로 구성된 단순 폼으로서 텍스트 입력창은 ng-model 지시자
를 사용해 사용자 입력을 컨트롤러의 scope.article 객체와 결합한다. 편집에 앞서 글 정보를 읽을 필요가 있으므로
뷰는 ng-init 지시자를 사용해 뷰가 올라올 때 컨트롤러의 findOne() 메소드를 호출한다. 또한 ng-submit 지시자를
form 엘리먼트에 지정한 사실에도 주목할 필요가 있다. 이 지시자는 Angular.js 에게 폼을 제출할 때 컨트롤러의
update() 메소드를 호출하게 알려준다. 이 뷰 역시 마지막에 error 를 보여 주도록 되어있다.
이제 마지막으로 이미 존재하는 글 목록을 보여주는 list-articles 뷰를 만들어보자. 이 뷰는 find() 메소드를 사용해
글의 컬렉션을 얻는다. 또한 ng-repeat 지시자를 사용해 각각 단일 글을 나타내는 HTML 구성 요소의 목록을 출력
할 것이다. 해당 글이 존재하지 않으면, 뷰는 사용자에게 create-article 뷰를 탐색하게 만든다.
이를 생성하기 위해 static/articles/views 폴더로 가서 list-article.client.view.html 이라는 파일을 만들고 코드를
붙여 넣자.
<section data-ng-controller="ArticlesController" data-ng-init="find()"> <h1>Articles</h1> <ul> <li data-ng-repeat="article in articles"> <a data-ng-href="#!/articles/{{article._id}}" data-ng-bind="article.title"></a> <br> <small data-ng-bind="article.created | date:'medium'"></small> <small>/</small> <small data-ng-bind="article.creator.username"></small> <p data-ng-bind="article.content"></p> </li> </ul> <div data-ng-hide="!articles || articles.length"> No articles yet, why don't you <a href="/#!/articles/create"> create one </a>? </div> </section>
list-articles 뷰는 글 목록을 나타내는 반복적인 HTML 엘리먼트의 단순 집합을 포함한다. 이 뷰는 ng-repeat 지시자
를 사용해 컬렉션에 존재하는 모든 글에 대한 목록 아이템을 중첩하며, ng-bind 지시자를 사용해 각 글 정보를
출력한다. 다른 뷰와 마찬가지로 , ng-controller 지시자를 사용해 ArticlesController 와 뷰를 연결한다.
그리고 글 목록을 올리기 위해 여기서도 ng-init 지시자를 사용해 뷰가 올라올때 find() 메소드를 호출한다.
해당 글이 없을 경우 사용자에게 새로운 글을 생성할지 묻기 위해 ng-hide 지시자를 사용한 방식에도 주목하자.
이제 뷰를 다 만들었기 때문에 각 뷰를 Angular.js 애플리케이션 라우팅 메커니즘에 연결할 필요가 있다.
이를 위해 각 뷰에 라우터를 명세할 필요가 있기에, static/articles 폴더로 가서 새로운 config 폴더를 생성하자.
config 폴더에 articles.client.routes.js 파일을 생성하고 다음 코드를 넣자.
angular.module('articles').config(['$routeProvider', function($routeProvider) { $routeProvider. when('/articles', { templateUrl: 'articles/views/list-articles.client.view.html' }). when('/articles/create', { templateUrl: 'articles/views/create-article.client.view.html' }). when('/articles/:articleId', { templateUrl: 'articles/views/view-article.client.view.html' }). when('/articles/:articleId/edit', { templateUrl: 'articles/views/edit-article.client.view.html' }); } ])
코드를 보면 각 뷰는 독자적인 라우트에 할당될 것 이다. 이미 존재하는 글을 표현하는 두개의 뷰 또한 URL 정의에
articleId 라우트 매개변수를 포함할 것이다. 이렇게 하면 컨트롤러가 $routeParams 서비스를 사용해 articleId 매개
변수를 추출할 수 있다.
이제 모듈구현을 마무리 하기 위해 모듈 자바스크립트 파일을 주 애플리케이션 페이지에 포함하고, 새로운 모듈
라우터에 적절한 링크를 보여주도록 app/views/ 폴더의 index.ejs 파일을 열어서 수정하자.
<!DOCTYPE html> <html xmlns:ng="http://angular.org"> <head> <title><%= title %></title> </head> <body> <!-- 제거 <% if (user) { %> <a href = "/signout">Sign out</a> <% } else { %> <a href="/signup">Signup</a> <a href="/signin">Signin</a> <% } %> --> <section ng-view></section> <script type="text/javascript"> window.user = <%- user || 'null' %> </script> <br> <img src="images/overwatchlogo.png" alt="logo"> <script type="text/javascript" src="/lib/angular/angular.js"></script> <script type="text/javascript" src="/lib/angular-route/angular-route.js"></script> <script type="text/javascript" src="/lib/angular-resource/angular-resource.js"></script> <script type="text/javascript" src="/articles/articles.client.module.js"></script> <!-- 추가 --> <script type="text/javascript" src="/articles/controllers/articles.client.controller.js"></script> <!-- 추가 --> <script type="text/javascript" src="/articles/services/articles.client.service.js"></script> <!-- 추가 --> <script type="text/javascript" src="/articles/config/articles.client.routes.js"></script> <!-- 추가 --> <script type="text/javascript" src="/example/example.client.module.js"></script> <script type="text/javascript" src="/example/controllers/example.client.controller.js"></script> <script type="text/javascript" src="/example/config/example.client.routes.js"></script> <script type="text/javascript" src="/users/users.client.module.js"></script> <script type="text/javascript" src="/users/services/authentication.client.service.js"></script> <script type="text/javascript" src="/application.js"></script> </body> </html>
새로운 모듈 자바스크립트 파일을 추가 하였다. 그리고 여기서 사용자 인증에 관한 부분은 제거 하였다.
그럼 이걸 어디다 넣느냐 . 예제 모듈의 첫 번째 화면에 두 링크를 추가 할 것 이다. 이를 위해
/static/example/views/ 폴더로 가서 example.client.view.html 파일을 열어서 변경하자.
<section ng-controller="ExampleController"> <div data-ng-show="!authentication.user"> <a href="/signup">Signup</a> <a href="/signin">Signin</a> </div> <div data-ng-show="authentication.user"> <h1>Hello <span data-ng-bind="authentication.user.username"></span></h1> <a href="/signout">Signout</a> <ul> <li><a href="/#!/articles">List Articles</a></li> <li><a href="/#!/articles/create">Create Article</a></li> </ul> </div> </section>
이제 예제 뷰는 사용자가 인증되지 않았을 때는 인증 링크 즉 로그인, 가입 링크를 보여주고 사용자가 로그인 한
다음에는 글 모듈 링크를 보여주도록 변경하였다. 이제 이 변경한 예제가 잘 작동하려면 , ExampleController 를
변경 할 필요가 있다. static/example/controllers 폴더로 가서 example.client.controller.js 파일을 열어서
Authentication 서비스 사용방식을 변경하자.
angular.module('example').controller('ExampleController', ['$scope', 'Authentication', function($scope, Authentication) { $scope.authentication = Authentication; } ]);
이렇게 변경하면 , 예제 뷰가 Authentication 서비스를 활용할 것 이다.
드디어 길고 긴 CRUD 모듈이 완성이 되었다. 이제 들뜬 마음으로 CRUD 기능을 테스트 해보자.
먼저 글을 생성해보자.
오랜만에 보는 로그인 화면이다. 메인화면에 들어오니 회원가입과 로그인 링크가 나타난다.
로그인을 해보자.
username 이 나타나고 로그아웃 링크와 글의 전체목록을 보는 ListArticles , 그리고 글을 생성하는 CreateArticle
링크가 보인다.
먼저 ListArticles 쪽으로 들어가보자.
글이 없다고 글을 생성하겠냐는 문장이 나오고 create one 이라는 새로운 글 생성 화면으로 가는 링크가 있다.
링크를 눌러 글을 생성해보자.
글 제목과 내용을 적는 란과 submit (제출) 버튼이 보인다. 글 제목과 내용을 적고 제출을 해보자.
글이 저장됨과 동시에 리스트 화면으로 돌아오며 방금 적은 글이 나타나고 시간과, 작성자의 username 역시
같이 나타난다. 이 상태에서 다시 리스트 화면으로 가보자.
(현재 돌아가기 버튼이 없는 관계로 다시 초기 주소로 가자.)
글 목록 화면에서 방금 적은 글이 보인다. 제목을 클릭화면 view-article 뷰로 넘어 갈 것이다.
그리고 view-article 에서 edit 와 delete 기능이 제대로 작동하는지도 테스트 해보자.
또한, 작성자가 아니라면 edit, delete 기능은 쓸 수 없기 때문에 다른 아이디로 들어가서 해당 글을 보고
edit, delete 링크가 나타나지 않는지 확인하자.
다른 아이디로 봤을 경우 위 처럼 edit,delete 가 나타나지 않아야 한다.
모든 것을 테스트 했다면 이제 로그인기능과 로그인인증, 그리고 글을 적고 수정하는 article CRUD 기능까지 완성
했다. 이제 점점 이 프로젝트도 끝이 보인다. 헥헥.
그리고 디자인 부분은 나중에 bootstrap 으로 좀더 이쁘게 꾸밀 것 이다. 지금은 기능만 확인하자.
다음 장 부터는 여기에 socket.io 를 이용하여 실시간 채팅기능을 넣는 것을 구현 할 것이다.
끝으로 이번장에서 추가된 articles.client.controller.js 파일의 전체코드를 보자.
angular.module('articles').controller('ArticlesController',['$scope', '$routeParams','$location','Authentication','Articles', function($scope,$routeParams,$location,Authentication,Articles) { $scope.authentication = Authentication; $scope.create = function() { var article = new Articles({ title : this.title, content : this.content }); article.$save(function(response) { $location.path('articles/' + response._id); }, function(errorResponse) { $scope.error = errorResponse.data.message; }); }; $scope.find = function() { $scope.articles = Articles.query(); }; $scope.findOne = function() { $scope.article = Articles.get({ articleId : $routeParams.articleId }); }; $scope.update = function() { $scope.article.$update(function(){ $location.path('articles/' + $scope.article._id); }, function(errorResponse){ $scope.error = errorResponse.data.message; }); }; $scope.delete = function(article) { if (article) { article.$remove(function() { for (var i in $scope.articles) { if($scope.articles[i] === article) { $scope.articles.splice(i, 1); } } }); } else { $scope.article.$remove(function() { $location.path('articles'); }); } }; } ]);
(끝)
'MeanStack (deprecated)' 카테고리의 다른 글
21장 Angular.js , Express (앵귤러js, 익스프레스) CRUD 모듈 만들기 (8) 2016.07.20 20장 Node.js express 를 이용한 CRUD 모듈 (0) 2016.07.13 19장 Angular.js (앵귤러js) 의 인증관리 (0) 2016.07.12 18장 Angular.js 를 이용한 라우팅 처리 (0) 2016.07.07 17장 Angular.js 를 이용한 MVC 엔티티 (2) 2016.07.05