대기시간 보정(Latency Compensation)

Sidebar 7.5

번역 완료율

이 장에서는:

  • 대기시간 보정에 대하여 이해한다.
  • 앱에 대한 진도를 잠시 멈추고 깊이있게 들여다본다.
  • 미티어 Method가 서로 호출하는 방법을 배운다.
  • 이전 장에서 우리는 미티어 세계의 새로운 개념인 메서드(Method)에 대하여 소개했다.

    대기시간 보정이 없을 때
    대기시간 보정이 없을 때

    미티어 Method는 서버에 있는 일련의 명령어들을 구조화된 방식으로 실행하는 수단이다. 이전 예제에서 Method를 사용한 이유는 새 post에 등록일시뿐 아니라 저자의 이름과 id를 함께 저장하기를 원했기 때문이다.

    그런데, Method를 가장 기본적인 방식으로 실행하면 문제가 생긴다. 다음 일련의 이벤트에 대하여 숙고해보자(주의: 시간은 설명을 위해서만 무작위로 정한 값이다):

    • +0ms: 사용자가 등록 버튼을 누르고 브라우저는 Method를 호출한다.
    • +200ms: 서버가 Mongo 데이터베이스에 변경내용을 저장한다.
    • +500ms: 클라이언트가 변경 내용을 받고 이를 UI에 반영한다.

    미티어가 이렇게 동작한다면, 각 단계별 작업과 그 결과를 보는 시간 사이에 약간의 지체(이 시간 지체는 사용자와 서버사이의 거리에 다소 의존성이 존재한다)가 발생한다. 현대적 웹 애플리케이션에서는 이를 용납할 수 없다!

    대기시간 보정

    대기시간 보정이 있을 때
    대기시간 보정이 있을 때

    이 문제점을 피하기 위하여, 미티어는 대기시간 보정(Latency Compensation)이라는 개념을 도입했다. 우리는 post Method를 정의하여 collections/ 디렉토리에 있는 파일에 넣었다. 이렇게 하면 서버와 그리고 클라이언트 양쪽에서 이용할 수 있다 - 그리고 동시에 양쪽에서 구동될 수 있다!

    Method를 호출하는 순간, 클라이언트는 그 요청을 서버에 보내는 동시에 그 로컬 컬렉션에서 그 Method의 작업을 흉내낸다. 그래서 워크플로우는 다음과 같이 된다:

    • +0ms: 사용자가 등록버튼을 클릭하고 브라우저는 Method를 호출한다.
    • +0ms: 클라이언트는 로컬 컬렉션에 Method 호출의 작용을 흉내내어 그 변경내용을 UI에 반영한다.
    • +200ms: 서버는 Mongo 데이터베이스에 변경 내용을 저장한다.
    • +500ms: 클라이언트는 그 변경내용을 받고 흉내낸 변경 작업을 취소한 다음, 서버의 변경내용(일반적으로 서로 같다)으로 바꿔치기한다. UI의 변경은 이를 반영한다.

    이 결과로 사용자는 변경이 순간적으로 이루어지는 것으로 인식한다. 서버의 반응이 약간의 시차를 두고 리턴할 때, 서버의 canonical 도큐먼트가 내려오면 눈에 띄는 변화가 있을 수도 있고 그렇지 않을 수도 있다. 여기서 한 가지 배울 것은 실제 도큐먼트를 우리가 할 수 있는 한 최대한 비슷하게 흉내를 내어야 한다는 것이다.

    대기시간 보정 관찰하기

    이를 실제로 보기 위해서 post Method 호출을 살짝 바꿔보자. 우리는 Method의 객체의 삽입 시간을 지체시키는 futures npm package를 이용하여 다소 고급 코딩을 시도할 것이다.

    isSimulation을 사용하여 미티어에게 Method가 현재 stub으로 구동된 것인지를 묻는다. “실제” Method는 서버에서 구동되는 반면, stub는 미티어가 클라이언트에서 병행적으로 실행하는 Method simulation이다.

    그러므로, 우리는 미티어에게 이 코드가 클라이언트에서 실행되는 지를 묻는다. 만약 그렇다면, 문자열 (client)을 글 제목의 끝부분에 추가하고, 그렇지 않으면 문자열 (server)을 추가한다:

    Posts = new Mongo.Collection('posts');
    
    Meteor.methods({
      postInsert: function(postAttributes) {
        check(this.userId, String);
        check(postAttributes, {
          title: String,
          url: String
        });
    
        if (Meteor.isServer) {
          postAttributes.title += "(server)";
          // wait for 5 seconds
          Meteor._sleepForMs(5000);
        } else {
          postAttributes.title += "(client)";
        }
    
        var postWithSameLink = Posts.findOne({url: postAttributes.url});
        if (postWithSameLink) {
          return {
            postExists: true,
            _id: postWithSameLink._id
          }
        }
    
        var user = Meteor.user();
        var post = _.extend(postAttributes, {
          userId: user._id, 
          author: user.username, 
          submitted: new Date()
        });
    
        var postId = Posts.insert(post);
    
        return {
          _id: postId
        };
      }
    });
    
    collections/posts.js

    우리가 여기서 멈춘다면, 이 시연에서 결정적인 모습은 보이지 않을 것이다. 이 시점에서, post 등록폼은 사용자에게 redirect하기 전에 5초간 쉬었다가 post 목록으로 되돌아가는 것일 뿐 별다른 일이 일어나지 않는다.

    그 이유를 이해하기 위해서, post 등록 이벤트 핸들러로 되돌아가보자:

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        };
    
        Meteor.call('postInsert', post, function(error, result) {
          // display the error to the user and abort
          if (error)
            return alert(error.reason);
    
          // show this result but route anyway
          if (result.postExists)
            alert('This link has already been posted');
    
          Router.go('postPage', {_id: result._id});  
        });
      }
    });
    
    client/templates/posts/post_submit.js

    우리는 Router.go()를 method 호출의 콜백 내부에서 호출하였다. 이것은 폼이 redirect하기 전에 method의 성공을 기다리는 것을 의미한다.

    이것은 올바른 작동이 구현되는 과정이다. 결론은 그 post 등록의 결과가 유효한지 아닌지를 알기 전에 사용자를 redirect 시킬 수 없다는 사실이다. 왜냐하면, 한 번 redirect했다가 몇 초 안에 데이터를 수정하도록 다시 원래의 post 등록폼 페이지로 되돌린다면 매우 혼란스러울 것이기 때문이다.

    그러나, 이 예제에서 우리는 그 등록 결과를 즉시 보기를 원한다. 그러므로 우리는 라우팅 호출을 postsList 루트로 redirect하도록 바꾸고 (우리는 post로 루트를 지정하지는 못한다. 왜냐하면 그 method 외부에서는 _id값을 알 수 없기 때문이다.) 콜백에서 꺼내어 무슨 일이 일어나는 지 보려고 한다:

    Template.postSubmit.events({
      'submit form': function(e) {
        e.preventDefault();
    
        var post = {
          url: $(e.target).find('[name=url]').val(),
          title: $(e.target).find('[name=title]').val()
        };
    
        Meteor.call('postInsert', post, function(error, result) {
          // display the error to the user and abort
          if (error)
            return alert(error.reason);
    
          // show this result but route anyway
          if (result.postExists)
            alert('This link has already been posted');
        });
    
        Router.go('postsList');  
    
      }
    });
    
    client/templates/posts/post_submit.js

    Commit 7-5-1

    Sleep 기능을 사용하여 post 목록이 나타나는 순서를 보여준다.

    이제 새 post를 등록하면, 대기시간 보정을 명백하게 관찰할 수 있다. 우선 post는 제목(GitHub로 링크가 잡힌 목록의 첫 post)에 (client)이 추가되어 나타난다:

    처음 클라이언트 컬렉션에 저장된 post
    처음 클라이언트 컬렉션에 저장된 post

    그리고, 5초가 지나면, 서버에 등록된 실제 도큐먼트로 대체된다:

    서버 컬렉션으로부터 수정본을 받은 후의 post
    서버 컬렉션으로부터 수정본을 받은 후의 post

    클라이언트 컬렉션 Methods

    이 과정을 공부하고 나서 메서드가 복잡하다고 생각할 지 모르겠지만, 사실 메서드는 아주 단순하다. 실제로 우리는 이미 아주 단순한 3개의 메서드를 본 적이 있다: 컬렉션을 다루는 메서드인 insert, update 그리고 remove이다.

    ‘posts'라는 이름의 서버 컬렉션을 정의할 때, 이미 3개의 메서드를 은연중에 정의하는 것이다: posts/insert, posts/update 그리고 posts/delete. 다른 말로 표현하면, 클라이언트 컬렉션에서 Posts.insert()을 호출하는 것은 다음 두 가지 작업을 수행하는 대기시간 보정 메서드를 호출하는 것이다:

    1. 콜백 함수 allow 와 deny를 호출하여 변경을 가할 수 있는 지를 검사한다.
    2. 해당 데이터 저장소에 실제로 수정을 가한다.

    메서드를 호출하는 메서드

    이제까지의 과정을 계속 따라왔다면, post 메서드가 post를 등록할 때 또 다른 메서드 (posts/insert)를 호출하였다는 것을 알았을 것이다. 이것은 어떻게 작동할까?

    Simulation(메서드의 클라이언트 쪽 버전)이 실행중일 때, 우리는 insert의 simulation(그래서 클라이언트 컬렉션에 삽입한다)을 실행한다. 그러나 우리는 실제인 서버쪽의 insert는 호출하지 않는데, 그것은 post의 서버쪽 버전이 이 작업을 할 것이기 때문이다.

    결과적으로, 서버쪽의 post 메서드가 insert를 호출할 때, simulation에 대하여 걱정할 필요없이 그 등록과정은 부드럽게 진행된다.