컬렉션(Collection)

4

번역 완료율

이 장에서는:

  • 미티어의 핵심기능인 실시간 컬렉션에 대해 배운다.
  • 미티어의 데이터 동기화에 대한 작동원리를 이해한다.
  • 컬렉션을 템플릿에 통합한다.
  • 기본 프로토타입을 실제 동작하는 실시간 앱으로 바꾼다!
  • 1장에서, 우리는 미티어의 핵심 기능인 클라이언트와 서버 사이의 데이터의 자동 동기화에 대하여 언급한 바 있다.

    이 장에서는, 그 작동 과정에 대하여 세밀하게 알아보고 이를 가능하게 하는 핵심 기술인 미티어 컬렉션(Collection)의 동작을 살펴본다.

    컬렉션(collection)은 특별한 데이터 구조체로서 데이터를 영구 저장할 수 있는 서버의 MongoDB 데이터베이스에 저장한다. 그리고 이를 각 연결된 이용자 브라우저와 실시간으로 동기화한다.

    우리는 post를 영구적으로 저장하고 이들을 사용자들 사이에서 공유하도록 하려고 한다. 그래서 우리는 Posts라는 이름의 컬렉션을 만들어 저장한다.

    Collection은 어떤 앱에서나 중심적인 역할을 한다. 그러므로 가장 먼저 정의하여 lib 디렉토리에 넣는다. 우선 lib 디렉토리 내부에 collections/ 디렉토리를 만들고 여기에 여기에 posts.js 파일을 만든다. 그리고 아래 내용을 넣는다:

    Posts = new Mongo.Collection('posts');
    
    lib/collections/posts.js

    Commit 4-1

    Posts 컬렉션을 추가했다.

    Var을 적용할까 말까?

    미티어에서, var 키워드는 해당 객체의 영역(scope)을 현재의 파일로 제한한다. 여기서, 우리는 Posts 컬렉션을 앱 전체에서 이용하기를 원한다. 이것이 우리가 var 키워드를 사용하지 않는 이유이다.

    데이터 저장

    웹 앱은 데이터의 처리에 있어 3가지 기본적인 방식을 가지고 있다. 그리고 그 각각은 다른 역할을 가진다:

    • 브라우저 메모리: JavaScript 변수같은 것들은 브라우저 메모리에 저장된다. 이것은 영구적이지 않다는 의미이다: 이것은 현재 브라우저 탭에 한정된다. 그리고 그 탭을 닫는 순간 사라진다.
    • 브라우저 저장소(storage) 브라우저는 데이터를 쿠키나 로컬 스토리지에 보다 영구적으로 저장할 수 있다. 여기 저장된 데이터는 세션 한계를 넘어서 저장할 수 있지만, 현재 이용자에 한정되며 (하지만 브라우저 탭의 한계는 벗어났다) 다른 사용자들과 쉽게 공유하지 못한다.
    • 서버 데이터베이스 영구적으로 데이터를 저장하는 최상의 장소로서 한 사용자에만 한정되지 않고 이용할 수 있는 곳은 전통적인 데이터베이스이다 (MongoDB는 미티어 앱에서의 기본 솔루션이다).

    미티어는 이 모두를 사용하며, (곧 보겠지만) 때로는 한 장소에서 다른 곳으로 데이터를 동기화한다. 말하자면, 데이터베이스는 데이터의 원본을 저장하는 “정통(canonical)” 데이터 소스로 존재한다.

    클라이언트와 서버

    client/server/가 아닌 폴더에 존재하는 코드는 양쪽에서 실행된다. 그러므로 Posts 컬렉션은 클라이언트와 서버 양쪽에서 이용할 수 있다. 그런데, 각 환경에서 컬렉션이 동작하는 방식은 완전히 다르다.

    서버에서, 컬렉션은 MongoDB 데이터베이스와 연결되어 데이터 입출력을 처리한다. 이런 측면에서 이것은 표준 데이터베이스 라이브러리와 비교할 수 있다.

    그런데 클라이언트에서는, 컬렉션은 실제 정통 컬렉션의 부분집합의 복제본이다. 클라이언트 쪽의 컬렉션은 그 부분집합을 지속적으로 그리고 (대개는) 명백하게 최신의 상태로 실시간으로 유지한다.

    콘솔 대 콘솔 대 콘솔

    이 장에서는, 브라우저 콘솔을 사용하여 시작하는 데, 이것을 터미널이나 Mongo 쉘(shell)과 혼동하면 안된다. 아래는 각각에 대한 속성 입문서이다.

    터미널

    터미널
    터미널
    • 운영체제에서 구동된다.
    • 서버에서의 console.log()를 호출하면 여기로 출력된다.
    • 프롬프트: $.
    • 다른 이름: Shell, Bash

    브라우저 콘솔

    브라우저 콘솔
    브라우저 콘솔
    • 브라우저 내부에서 구동되어, JavaScript 코드를 실행한다.
    • 클라이어트에서의 console.log()를 호출하면 여기로 출력된다.
    • 프롬프트: .
    • 다른 이름: JavaScript Console, DevTools Console

    Mongo 쉘

    Mongo 쉘
    Mongo 쉘
    • 터미널에서 meteor mongo 명령어를 실행하여 구동된다.
    • 앱의 데이터베이스에 직접 접속한다.
    • 프롬프트: >.
    • 다른 이름: Mongo Console

    각각의 경우에 프롬프트 문자 ($, , or >)를 명령어의 일부로 입력해야 하는 것은 아니다. 그리고 프롬프트로 시작하지 않는 라인은 이전 명령의 출력 결과라고 보면 된다.

    서버에서의 컬렉션

    서버로 돌아가서, 컬렉션은 Mongo 데이터베이스로의 API로 기능한다. 서버쪽 코드를 작성할 때, Posts.insert() 또는 Posts.update()와 같은 Mongo 명령어를 쓸 수 있고, 이들은 Mongo에 저장된 posts 컬렉션을 변경한다.

    Mongo 데이터베이스의 내부를 보려면, 새로운 터미널 창을 열고 (현재 meteor가 구동 중인 터미널 창은 그대로 둔 채로), 앱이 있는 디렉토리로 이동한다. 그리고, 명령어 meteor mongo를 실행하여 Mongo shell을 구동하라. 여기에서 표준 Mongo 명령어를 입력할 수 있다(그리고, ctrl+c 단축키로 빠져 나온다). 예를 들어 아래와 같이 입력해보자:

    meteor mongo
    
    > db.posts.insert({title: "A new post"});
    
    > db.posts.find();
    { "_id": ObjectId(".."), "title" : "A new post"};
    
    Mongo 쉘

    Meteor.com에서의 Mongo

    *.meteor.com에서 앱을 호스팅하면, 배포된 앱의 Mongo 콘솔은 명령어 meteor mongo myApp으로 접속할 수 있다.

    그리고 거기에 있는 동안에는, 앱의 로그파일도 meteor logs myApp명령으로 볼 수 있다.

    Mongo의 문법도 Javascript 인터페이스를 사용하므로 익숙하다. 우리가 Mongo 콘솔에서 더 이상의 데이터 작업을 하지는 않겠지만, 때때로 무슨 일이 일어나는지 엿볼 수는 있다.

    클라이언트에서의 컬렉션

    클라이언트에서 컬렉션은 좀 더 흥미롭다. 클라이언트에서 Posts = new Mongo.Collection('posts'); 라고 선언하는 것은, 실제 Mongo 컬렉션의 로컬, 인-브라우저 캐시를 생성하는 것이다. 우리가 클라이언트 쪽의 컬렉션을 “캐시"라고 말하는 것은, 데이터의 부분 집합을 가지며, 데이터에 빠르게 접근할 수 있다는 것을 의미한다.

    이 부분을 이해하는 것이 이것이 미티어가 작동하는 방식의 기본이라는 점에서 중요하다. 일반적으로, 클라이언트 쪽 컬렉션은 Mongo 컬렉션에 저장된 전체 도큐먼트의 부분집합이다.(결국, 우리는 전체 데이터베이스를 클라이언트로 보내는 것을 원하지 않는다).

    두 번째, 이 도큐먼트들은 브라우저 메모리에 저장되는 데, 이는 여기에 접근하는 것이 기본적으로 순간적이라는 것을 의미한다. 그러므로 클라이언트에서 Posts.find()를 호출할 때, 데이터를 가져오려고 서버나 데이터베이스에 느리게 갔다오는 일은 없다. 데이터는 이미 로드되어 있기 때문이다.

    MiniMongo 소개

    미티어의 클라이언트쪽 Mongo 구현체를 MiniMongo라 부른다. 이는 완벽한 구현체는 아니어서, 때로 MiniMongo에서 구현되지 않는 Mongo 기능을 접할 수도 있다. 그래도, 이 책에서 다루는 모든 기능은 Mongo와 MiniMongo 양쪽 모두에서 유사하게 동작한다.

    클라이언트-서버 통신

    여기서 핵심부분은 클라이언트의 컬렉션이 같은 이름(여기서는 'posts')의 서버 컬렉션과 동기화하는 방법이다.

    이에 대한 상세한 설명보다는, 그저 무슨 일이 일어나는지 지켜보기 바란다.

    두 개의 브라우저 윈도우를 열고, 각각에서 Javascript 콘솔을 연다. 그리고, 터미널에서 Mongo 콘솔을 연다.

    이 때, 이 세 개의 창에서 이전에 우리가 만든 단일 도큐먼트를 볼 수 있다 (앱의 UI 에서는 여전히 이전의 3개의 더미 post를 볼 수 있지만 이것은 무시하자).

    > db.posts.find();
    {title: "A new post", _id: ObjectId("..")};
    
    Mongo 쉘
     Posts.findOne();
    {title: "A new post", _id: LocalCollection._ObjectID};
    
    브라우저 콘솔 1

    이제, 새로운 post를 등록한다. 브라우저 창의 하나에서 아래 명령을 실행한다:

     Posts.find().count();
    1
     Posts.insert({title: "A second post"});
    'xxx'
     Posts.find().count();
    2
    
    브라우저 콘솔 1

    예상한대로, post는 로컬 컬렉션에 저장된다. 이제 Mongo에서 확인해보자:

    ❯ db.posts.find();
    {title: "A new post", _id: ObjectId("..")};
    {title: "A second post", _id: 'yyy'};
    
    Mongo 쉘

    보는 바와 같이, 클라이언트에서 서버로 보내는 한 줄의 코드 작성없이도 post를 Mongo 데이터베이스로 삽입했다(뭐, 엄격하게 말하면, 딱 줄의 코드를 작성하긴 했다: new Meteor.Collection('posts')). 하지만 이게 다는 아니다!

    두 번째 브라우저를 열고, 브라우저 콘솔에서 아래 명령을 실행하여 보자:

     Posts.find().count();
    2
    
    브라우저 콘솔 2

    여기에도 post가 있다! 이 두 번째 브라우저를 새로고침하거나 여기서 무슨 작업을 하지 않았어도, 어떤 코드를 작성하지 않았어도 그렇다. 이것은 마술처럼 그리고 순간적으로 일어났다. 나중에 이에 대하여 보다 명백하게 알게 될 것이다.

    무슨 일이 일어났는고 하니, 새로운 post가 등록된 클라이언트쪽의 컬렉션이 서버쪽 컬렉션에게 알린 것이다. 그리고, 그 post를 Mongo 데이터베이스에 삽입하고, 연결된 모든 다른 post 컬렉션들에게도 알려준 것이다.

    브라우저 콘솔에서 Posts를 가져오는 것은 실용적이지는 않다. 우리는 이 데이터를 템플릿으로 보내는 방법을 배울 것이고, 이 과정에서 단순한 HTML 프로토타입을 작동하는 실시간 웹 애플리케이션으로 바꿀 것이다.

    데이터베이스 활용

    브라우저 콘솔에서 컬렉션의 내용을 열람하는 것은 그저 한 기능일 뿐이다. 우리가 진정 원하는 것은 화면에서 데이터를 보여주고, 그 데이터가 변경되는 모습을 보여주는 것이다. 그렇게 함으로써, 우리 앱을 정적인 데이터를 보여주는 단순 웹 페이지에서 동적으로 변경되는 데이터를 보여주는 실시간 웹 애플리케이션으로 바꾸게 될 것이다.

    우리의 첫 과제는 데이터베이스에 데이터를 넣는 것이다. 우리는 서버가 처음 기동할 때 구조화된 데이터를 Posts 컬렉션에 넣는 초기 데이터 파일(fixture file)을 사용하여 데이터를 넣을 것이다.

    우선, 데이터베이스를 비운다. meteor reset을 이용하여, 데이터베이스를 지우고 프로젝트를 리셋한다. 물론 이 명령어를 실행하는 것은 실제 프로젝트에서 작업을 진행할 때에는 매우 조심스럽게 해야 한다.

    미티어 서버를 중지시킨(ctrl-c를 누른다) 다음 커맨드 라인에서 다음을 실행한다:

    meteor reset
    

    이 reset 명령어는 Mongo 데이터베이스를 완전하게 비운다. 이것은 개발단계에서는 유용한 명령어이지만, 데이터베이스가 불일치 상태에 빠지게 될 가능성이 높다.

    이제 다시 미티어 앱을 구동시킨다:

    meteor
    

    데이터베이스가 비었으니 이제 아래 코드를 추가하여 서버가 구동되고 Posts 컬렉션이 빈 상태일 때마다, 3개의 post가 데이터베이스에 저장되도록 한다:

    if (Posts.find().count() === 0) {
      Posts.insert({
        title: 'Introducing Telescope',
        url: 'http://sachagreif.com/introducing-telescope/'
      });
    
      Posts.insert({
        title: 'Meteor',
        url: 'http://meteor.com'
      });
    
      Posts.insert({
        title: 'The Meteor Book',
        url: 'http://themeteorbook.com'
      });
    }
    
    server/fixtures.js

    Commit 4-2

    Posts 컬렉션에 데이터를 추가했다.

    우리는 이 파일을 server/ 디렉토리에 넣었으므로, 이는 사용자 브라우저에는 로드되지 않을 것이다. 이 코드는 서버가 구동될 때 바로 실행되어 Posts 컬렉션에 3개의 post를 추가하도록 데이터베이스에 insert 요청을 한다. 아직은 어떠한 데이터 보안 처리를 하지 않았으므로, 이 파일이 서버에서 구동되나, 브라우저에서 구동되나 차이는 없다.

    이제 meteor 명령으로 서버를 다시 구동한다. 그리고 이 3개의 post는 데이터베이스에 삽입될 것이다.

    동적 데이터

    이제 브라우저 콘솔을 열어, MiniMongo에 3개의 post가 로드된 것을 볼 수 있다:

     Posts.find().fetch();
    
    브라우저 콘솔

    이 post들을 HTML에 보이기 위해서, 템플릿 헬퍼를 사용한다.

    3장에서 우리는 미티어에서 단순 데이터 구조의 HTML 뷰를 구축하기 위하여 데이터 컨텍스트를 Spacebars 템플릿에 엮는 방법을 적용해 보았다. 컬렉션 데이터도 같은 방법으로 엮을 수 있다. 단지, 정적인 postsData Javascript 데이터 객체를 동적인 컬렉션으로 바꾸기만 하면 된다.

    안그래도, postsData 코드는 편하게 삭제한다. 이제 posts_list.js 파일의 코드는 다음과 같을 것이다:

    Template.postsList.helpers({
      posts: function() {
        return Posts.find();
      }
    });
    
    client/templates/posts/posts_list.js

    Commit 4-3

    컬렉션을 `postsList` 템플릿과 연동했다.

    찾기(Find)와 가져오기(Fetch)

    미티어에서, find()커서를 리턴하는데, 이는 반응형 데이터 소스이다. 우리가 그 데이터의 내용을 얻으려고 할 때, 현재 커서 위치에서 데이터를 배열로 변환하는 fetch()를 사용한다.

    앱 내부에서, 미티어는 똑똑하게도 이를 배열로 변환하지 않고도 커서를 따라서 반복하는 방법을 안다. 이런 이유로 실제 미티어 코드에서 fetch()는 잘 보이지 않는다(그리고 위의 예에서 사용하지 않은 이유이기도 하다).

    변수에 지정된 정적 배열로부터 post의 목록을 가져오는 대신, 이제 posts 헬퍼에 대한 커서를 리턴한다 (같은 데이터를 사용하므로 별로 달라지는 것은 없을 것이다):

    라이브 데이터 사용하기
    라이브 데이터 사용하기

    {{#each}} 헬퍼가 Posts 전체를 반복하여, 이들을 화면에 뿌려주었다. 서버쪽의 컬렉션은 Mongo로부터 post 목록을 추출하여, 이들을 클라이언트쪽의 컬렉션에 넘기고, Spacebars 헬퍼가 이들을 템플릿으로 전달한 것이다.

    이제, 한 단계를 더 나가자; 브라우저 콘솔에서 새로운 post를 추가해보자:

     Posts.insert({
      title: 'Meteor Docs', 
      url: 'http://docs.meteor.com'
    });
    
    브라우저 콘솔

    다시 브라우저를 보면 다음과 같이 나타난다:

    콘솔에서 post 추가하기
    콘솔에서 post 추가하기

    독자는 처음으로 반응형으로 동작하는 것을 본 것이다. 우리가 Spacebars에게 Posts.find() 커서를 통해서 반복하도록 지시했을 때, 이것은 지속적으로 변화를 관찰하면서, 그 HTML을 가장 간단한 방식으로 수정하여 화면에 올바른 데이터를 보여준다.

    DOM의 변화를 살펴보기

    이 경우에, 가능한 가장 간단한 변경은 새로운 <div class="post">...</div>를 추가하는 것이다. 실제로 이렇게 되는 것을 보고 싶다면, DOM inspector를 열고 기존의 post들중의 하나에 대응하는 <div>를 선택하면 된다.

    이제, JavaScript 콘솔에서 또 다른 post를 삽입한다. 그리고 inspector를 다시 보면, 새 post에 대응하는 추가된 <div>를 볼 수 있지만, 여전히 동일한 기존의 <div>가 선택된 상태로 있는 것을 볼 수 있다. 이것이 언제 엘리먼트들이 화면에 다시 그려지고 언제 그대로 있는지를 알 수 있는 유용한 방법이다.

    컬렉션에 접속하기: 발행(Publication)과 구독(Subscription)

    지금까지는 autopublish 패키지를 활성화시킨 상태였는데, 이 패키지는 실서비스용이 아니다. 그 이름에서 알 수 있듯이, 이 패키지는 각 컬렉션마다 그 전체를 연결된 각 클라이언트와 공유하게 한다. 이는 우리가 진정 원하는 바가 아니므로, 이 기능을 끄도록 한다.

    터미널 창을 열고 아래와 같이 입력한다:

    meteor remove autopublish
    

    이렇게 하면 바로 효과가 나타난다. 브라우저를 보면, post가 모두 사라졌을 것이다! 이것은 우리가 post에 대한 클라이언트 쪽의 컬렉션이 데이터베이스에 있는 모든 post의 미러가 되도록 autopublish에 의존해왔기 때문이다.

    결국, 우리는 사용자가 실제로 (페이징같은 것을 고려하여) 보고 싶어하는 post만을 전달하면 된다는 점을 확실하게 해두자. 하지만 당장은 Posts 전체를 발행(publish)하도록 설정할 것이다.

    이렇게 하기 위해서, 우리는 publish() 함수를 만들어 모든 post를 참조하는 커서를 리턴하게 한다:

    Meteor.publish('posts', function() {
      return Posts.find();
    });
    
    server/publications.js

    클라이언트에서, 이 발행(publication)을 구독(subscribe)해야 한다. 아래 라인을 main.js에 추가하기만 하면 된다:

    Meteor.subscribe('posts');
    
    client/main.js

    Commit 4-4

    패키지 `autopublish`를 삭제하고 기본적인 발행을 작성했다.

    다시 브라우저를 보면, post 목록이 모두 돌아왔다. 휴!

    결론

    그래서 우리는 무엇을 이루었을까? 아직 사용자 인터페이스를 다루지는 않았지만 이제 정상적으로 동작하는 웹 애플리케이션을 얻었다. 이 애플리케이션을 인터넷에 배포할 수도 있다. 그리고 (브라우저 콘솔을 이용하여) 새로운 이야기를 올리고 이것이 전 세계 다른 사람들의 브라우저에 나타나는 것을 볼 수 있게 되었다.