원문: https://www.fusetools.com/docs/tutorial/mock-backend

소개

이 전 챕터 에서는, Fuse의 네비게이터라우터 클래스를 살펴본 후, 그것들을 사용하여 앱의 뷰 를 하나로 묶어 그들 사이에 데이터를 전송했습니다. 그러나 지금까지 EditHikePage 는 실제 데이터 모델을 영구적으로 변경할 수 없었습니다. 예를 들어, 하이킹을 선택하고 변경 한 다음 다른 하이킹으로 이동 한 후 다시 탐색하면, 변경 한 내용이 손실되어 있습니다. 이 것은 변경 사항들이 EditHikePage 의 뷰 모델에서만 로컬로 만들어질 뿐, 우리의 모델에는 전혀 반영 되지 않기 때문입니다.

그러나 이제는 우리가 아키텍처에 대해 더 많이 알게 되었으므로, 이 문제를 해결할 시점입니다. 실제 백엔드처럼 작동하는 모듈인 모의(mock) 백엔드 를 구현함으로써 그렇게 할 것입니다. 그러나 디바이스의 저장소나 데이터베이스 어딘가에 영구 보관하는 대신, 그냥 실행중인 응용 프로그램에 일부 데이터를 로컬로 저장만 할 것입니다.

Fuse를 사용하여 앱을 제작할 때, 이와 같은 모의 백엔드를 만드는 것이 반드시 필요한 것은 아닙니다. 예를 들어, 기존 백엔드 솔루션을 선택할 수도 있고, 이를 직접 구현할 수도 있습니다. 그러나 이 튜토리얼은 가능한 한 일반적인 상황을 위한 것이기 때문에, 우리는 특정 백엔드의 디테일들 보다 핵심 컨셉들에 집중 할 수 있는, 지금 이런 일반적인 백엔드-독립적인 방식 컨셉들을 원합니다. 이는 백엔드 솔루션이 실제로 사용되는지 여부에 관계없이, 향후 앱을 실제 백엔드에 연결할 때 예상할 수 있는 것을 이해할 수 있도록, 최소 한 번 이상 해보는 것이 좋은 연습이 될 것입니다.

참고: 이 튜토리얼 시리즈를 완성한 후에는, 우리의 모의(mock) 백엔드를 특정 백엔드 솔루션 및 기타 많은 멋진 기능과의 통합으로 대체하는 것을 다루는 다양한 "트랙들" 로 확장 할 예정입니다!

또한, 우리의 뷰 모델들이 상호 작용 하게 될 모의(mock) 백엔드를 직접 사용하는 대신, 모의 백엔드 위에 얇은 추상화를 만듭니다. 이렇게하면 모의 백엔드 작동 방법을 변경하거나 실제 백엔드로 교체하는 경우, 뷰 모델들 중 어떤 것도 변경할 필요가 없게 됩니다. 새로운 백엔드와 상호 작용하도록 추상화를 업데이트 할 수 있으며, 이전과 동일한 인터페이스를 계속 제공 할 수 있습니다. 객체 캐싱과 같은 [모의] 백엔드가 가지고 있지 않은 기능을 채워서 이러한 추상화가 가지는 장점을 활용할 수도 있습니다.

우리가 뭔가 만드는 것을 시작하기 전에, 일반적인 백엔드 인터페이스 는 어떤지 고려해 볼 필요가 있습니다. 살펴봅시다!

이 장의 마지막 코드는 여기 에서 볼 수 있습니다.

일반적인 백엔드 인터페이스

백엔드들은 꽤 복잡할 수 있고 그것들이 어떻게 보이고/작동하는지는 매우 다양할 수 있지만 아주 기본적인 인터페이스는 전반적으로 비슷하고, 특히 몇 가지 핵심 기능만 수행하면 더욱 그렇습니다. 예를 들면, 우리는 초기화, 가입, 인증 등과 같은 것들은 무시해도 되는데, 왜냐하면 그 부분들은 고도로 백엔드에 종속적이고, 우리 기본 앱에서는 관심을 두지 않는 기능들이기 때문입니다. 심플한 우리 앱의 경우, 간단한 데이터 저장/검색 그리고 해당 데이터를 업데이트하는 방법만 필요합니다. 이러한 기능들을 염두에 둔, 간단한 백엔드 인터페이스는 다음과 같이 보일 수 있습니다.:

// item 오브젝트들의 배열을 반환합니다
function getItems() { ... }
// item을 업데이트 합니다
function updateItem(...) { ... }

그럼, 우리 앱은 이 인터페이스를 매우 간단한 방법으로 사용합니다.:

// 백엔드로부터 item 오브젝트를 얻습니다
var someItems = getItems();
// 백엔드에서 item 중 하나를 업데이트 합니다
updateItem(...);

이정도로 간단합니다. 그러나 우리는 대부분의(전부는 아닐지라도) 백엔드가 처리해야하는 distribution (분배) 에 대한 매우 중요한 세부 사항을 무시했습니다. 우리의 단순한 인터페이스가 데이터를 이미 로컬에 가지고 있다면 잘 작동 할 동작하겠지만, 만약 데이터를 어딘가 다른 서버에 저장한다면 어떨까요? 백엔드 서버 (또는 디스크 등)에 요청한 데이터가 응답하기를 기다리면서 우리 코드가 그냥 멈춰 있게 할 수는 없습니다. 우리의 요청이 백엔드에 도착하고, 백엔드가 데이터를 다시 전송하도록 요청하는 데는 불명확한 시간이 걸릴 수 있습니다. 따라서 데이터 검색 및 업데이트는 비동기 적으로 이뤄져야 합니다. 그리고, 자바스크립트에서 어떤 비동기식 계산이 포함될 때, 여러분은 그 근처에서 Promise 를 몇 개 발견 할 가능성이 꽤 큽니다.

Promise 에 대한 MDN의 문서 를 의역하자면, "Promise 는 현재 또는 미래에 사용할 수 있는 값을 나타냅니다." 이것은 Promise 가 할 수 있는 일에 대한 아주 기본적인 설명이지만, 이미 이 설명에서 우리는, 백엔드와 비동기적으로 통신하려는 우리의 사용 사례에 부합한다는 것을 알 수 있습니다. Promise 들을 사용하면 일반적인 백엔드 인터페이스가 다음과 같이 보입니다.:

// item 오브젝트들의 배열을 나타내는 Promise를 반환 합니다
function getItems() { ... }
// 백엔드에서 item 이 업데이트 될때, 수행 될 Promise를 반환 합니다.
function updateItem(...) { ... }

이것으론 우리 인터페이스가 전혀 바뀌지 않은 것처럼 보입니다! 물론, 실제로 이러한 함수들을 사용 하는 것 (그리고 우리의 경우엔 모의 백엔드를 구현하는 것)은 약간은 다르지만, 많이 다르진 않습니다. 예를 들면 이 인터페이스를 사용하는 코드는 다음과 같을 수 있습니다.

// 비동기로 백엔드로부터 item 오브젝트를 얻습니다
var someItems = [];
getItems()
    .then(function(items) {
        someItems = items;
    })
    .catch(function(error) {
        console.log("Couldn't get items: " + error);
    });

// 비동기로 백엔드에서 item들 중 하나를 업데이트 합니다
updateItem(...)
    .catch(function(error) {
        console.log("Couldn't update item: " + error);
    })

Promise 를 제대로 사용하려면, 비동기 코드를 일부 도입해야 합니다. Promise 와 상호 작용하는 가장 간단한 방법은 그것이 제공하는 두 가지 함수, 즉, thencatch 를 호출하는 것입니다.

then 함수를 호출함으로써, Promise 가 수행 될 때 발생 될 작업을 기술 할 수 있습니다. 우리는 해당 Promise 가 채워진 값을 선택적으로(optionally) 받아들이는 또 다른 함수를 전달함으로써, 이 작업을 수행합니다; 이 경우엔, 백엔드의 items 입니다. 그러나 이 인수는 optional(선택 사항) 입니다. 예를 들면 우리의 updateItem 함수의 경우, 업데이트가 완료될때 그것이 반환 하는 Promise 는 값을 산출하기 위한 것이 아닙니다. 우리는 Promise 가 실제로 완료되었는지 아닌지 여부에만 관심이 있으므로, 이 경우는 인수가 사용되지 않습니다.

catch 함수를 호출하여, Promise 를 수행하는 동안 오류가 발생하면 어떻게 되는지를 기술 할 수 있습니다. 이 경우 해당 실패 원인이 무엇인지에 대한 설명을 수용하는 함수를 전달할 수 있습니다. 예를 들어 실제 백엔드가 백엔드 서버에 연결할 수 없거나 인증에 실패하면, 에러를 보고 할 수 있습니다. 에러 감지/처리 는 복잡한 주제이지만, 간단한 작업을 위해 이런 많은 세부 사항들을 대충 훑어 볼 것입니다. 우리는 여전히 몇 개의 간단한 에러 핸들러들을 Promise 들에 붙여, 나중에 실제 백엔드에 연결하는 경우에도 사용하기를 원합니다.

Promises 들은 많은 기능을 제공하며, 매우 유용합니다. 그러나 이 짧은 설명으로 모의 백엔드를 만들고 사용하는 것뿐만 아니라, 나중에 실제 백엔드들의 인터페이스들을 이해하기 위해 우리가 알아야 할 모든 것을 명확히 해야 합니다.

이제 여러분 중 일부는 흥미로운 것을 발견했을 겁니다. 그것은 우리가 이미 사용하고있는 Fuse의 Observable 들과 비슷한 Promise 입니다. Observable 은 변경 및 관찰 할 수 있는 값을 나타내며, 비동기 인터페이스에도 적합합니다. 실제로 우리가 원한다면 우리의 백엔드 인터페이스를 모의(mock) 하기 위해 Promise 들 대신 Observable 들을 사용할 수 있습니다. 그 의미는 매우 유사합니다. 그리고 특히 Fuse를 목표로 하는 백엔드를 제작한다면, 백엔드와의 통합이 쉽기 때문에 이것이 권장됩니다! 그러나 Fuse 보다 더 많은 플랫폼을 지원하기 위한 많은 백엔드 솔루션이 구축 되어있기 때문에, 우리는 Promise 를 제공하는 인터페이스와 상호 작용 할 가능성이 높습니다. 따라서 그런 경우라고 생각하고, 우리의 mock을 디자인 할 것입니다. 다행스럽게도 Promise 들과 Observable 들 사이의 유사점 때문에, 이 두 가지 사이의 틈을 메우는 것은 나중에 꽤 쉽게 할 수 있습니다.

참고: MDN Promise 가이드 에서 Promises 에 대한 자세한 내용을 배울 수 있고, Fuse 가 구체적으로 준수하는 Promise의 진수를 설명하는 A+ Promise 표준 을 볼 수 있습니다.

대체로 Promise 들을 사용하는 것이, 일반적인 JS기반 백엔드 인터페이스가 보여주는 것들과 꽤 근접한 결과를 도출하므로, 이를 모의 백엔드를 모델링하는데 사용 할 겁니다.

모의(mock) 백엔드 구현

참고: 우리 프로젝트에서 잠시 떠나 있을 것이므로, 한동안 컴파일이나 미리보기를 할 수는 없겠지만, 챕터가 끝나기 전까지는 백업하고 실행 할 것임을 알고 계십시오.

모의 백엔드 구현을 시작하기 전에, JavaScript를 어떻게 구성 할 것인지에 대해 이야기하고자 합니다. 현재 프로젝트 (물론 뷰 모델을 제외하고)에는 독립형 JS 모듈 하나만 있습니다. hikes.js 파일입니다. 더 많은 모듈들이 추가 될 것이기 때문에, 우리는 프로젝트가 훌륭하고 체계적으로 유지되도록 할 것입니다. 우리는 우리 앱의 서로 다른 페이지들과 관련된 모든 파일들을 배치했던 Pages 폴더와 유사하게, 우리 프로젝트의 루트에 Modules 폴더를 만들어서 우리의 모든 독립형 JS 모듈들을 배치 해 보겠습니다.:

.
|- MainView.ux
|- Modules
|- Pages
|  |- EditHikePage.js

...

다음으로 할 일은 Fuse가 이 디렉토리에 있는 모듈에 대해 알고 있는지 확인하는 것입니다. 이전에는 hikes.js 파일을 프로젝트에 추가 할 때, 프로젝트 파일 ( hikr.unoproj )에 이 파일에 대한 참조를 추가하여 응용 프로그램과 함께 번들로 제공했습니다. Modules 폴더를 만들었으므로, 이제는 해당 항목을 Module 디렉터리의 모든 JavaScript 파일을 번들로 포함하는 항목으로 바꿉니다.

...

  "Includes": [
    "*",
    "Modules/*.js:Bundle"
  ]
}

이제 모의 백엔드 구현을 시작 할 준비가되었습니다. 이전 hikes.js 파일에는 이미 제시해야 할 모든 데이터가 포함되어 있으므로 이를 시작점으로 사용할 수 있습니다. 먼저 파일을 Modules 디렉토리로 옮기고 그 이름을 Backend.js 로 바꿉니다.

새로운 Backend.js 파일을 살펴보면, 단순히 hikes 배열을 그대로 exports (내보내기) 합니다. 그러나 이제 이것이 우리의 모의 백엔드가 될 것이므로, 우리가 이전에 논의한 인터페이스와 유사한 인터페이스를 노출시키길 원합니다. 이것은 다음과 같이 보일 것입니다.:

// hike 오브젝트들의 배열을 나타내는 Promise를 반환합니다.
function getHikes() { ... }
// 백엔드에서 하이킹이 업데이트되면 수행 될 Promise를 반환합니다.
function updateHike(...) { ... }

먼저 getHikes 함수를 만듭니다. hikes 배열 밑에, 빈 함수부터 시작하겠습니다.:

function getHikes() {
}

이 함수가 hikes 배열을 반환하기를 원한다면, 다음과 같이 작성할 수 있습니다.:

function getHikes() {
    return hikes;
}

그러나 이 대신에, 우리는 hikes 가 어떤 백엔드로부터 그것들을 가져 오는 시뮬레이션을 할 준비가 되면 채워질, Promise 를 반환하는 함수를 원합니다. 그래서 우리의 실제 getHikes 함수는 다음과 같이 보일 것입니다.:

function getHikes() {
    return new Promise(function(resolve, reject) {
        resolve(hikes);
    });
}

이제 hikes 배열을 반환하는 대신, new Promise 를 사용하여 Promise 를 생성 합니다. Promise 생성자는 promise를 수행 하거나 거부 하기 위한 목적으로 호출 될 함수에서 사용합니다. 이 함수는 resolvereject 두 인수를 취합니다. 이 인수들은 실제로는 함수 자체입니다. resolve 는 우리가 만들고 있는 Promise 를 수행하는 함수이며, 만약 거기에 then 함수를 사용하여 Promise 에 핸들러를 첨부하면, 해당 핸들러가 호출됩니다. 우리 코드에서는, 이것이 Promisehikes 컬렉션으로 resolve 하기 위해 호출 해야 하는 함수 전부 입니다. 에러가 발생하면 해당 reject 함수가 대신 호출 될 수 있고, catch 를 통해 Promise 에 첨부 된 어떤 핸들러들은 이 후에 호출 될 것입니다.

우리는 또 실제 밀리 초 단위로 이것이 수행될 때 딜레이를 주기 위한 JS의 내장 setTimeout 함수를 사용 할 수 있습니다. setTimeout 도 두 개의 인수를 취합니다. 첫 번째 것은 미래에 언젠가 호출 될 함수이고, 두 번째 것은 해당 함수를 호출하기 전에 지연되는 지연시간(밀리 초)입니다. 예를 들어, 이 코드는 0.5 초 후 Promise 를 resolve 할 것입니다.:

function getHikes() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(hikes);
        }, 500);
    });
}

이 코드는 우리의 앱이 백엔드에서 오는 데이터를 기다릴 필요가 있을 때, 어떻게 처리되는지를 테스트해 보고자 하는 경우 매우 유용합니다. 그러나, 테스트하는 동안 자체적으로 단순하게 유지하려면, 딜레이 없이 0 을 사용하십시오.

function getHikes() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(hikes);
        }, 0);
    });
}

완벽한 인터페이스와 적절한 딜레이 시뮬레이션을 가진 멋진 getHikes 함수를 얻었습니다!

이제 updateHike 함수 차례입니다. 이것은 모의 백엔드에서 업데이트 할 특정 하이킹에 대한 정보를 가져와, 업데이트가 완료되면 수행 될 Promise 를 반환하는 함수입니다. 빈 함수에서 시작합니다.:

function updateHike() {
}

그런 다음, 특정 하이킹을 식별하고 업데이트 하기 위한 몇 가지 인수를 추가합니다.:

function updateHike(id, name, location, distance, rating, comments) {
}

이 경우, id 인수를 사용하여 업데이트 할 하이킹을 식별하고, 나머지 인수들은 해당 하이킹에 상응하는 필드들로 전부 덮어 쓰게 될 것입니다.

다음으로, Promise 생성자와 setTimeoutgetHikes 에 했던 것처럼 사용하여, 선택적인 시간 딜레이로 Promise 를 반환합니다.:

function updateHike(id, name, location, distance, rating, comments) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
        }, 0);
    });
}

좋아 보입니다! 이제 id 로 하이킹을 실제로 식별하고, 멤버를 업데이트하는 코드를 추가합니다. 작업을 심플하게 하기 위해, 우리가 찾고있는 하이킹을 찾기 위한 간단한 선형 검색을 할 것입니다.:

function updateHike(id, name, location, distance, rating, comments) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            for (var i = 0; i < hikes.length; i++) {
                var hike = hikes[i];
                if (hike.id == id) {
                }
            }
        }, 0);
    });
}

하이킹이 확인되면, 함수의 인수로 데이터를 업데이트하고 검색 루프를 빠져 나옵니다.:

function updateHike(id, name, location, distance, rating, comments) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            for (var i = 0; i < hikes.length; i++) {
                var hike = hikes[i];
                if (hike.id == id) {
                    hike.name = name;
                    hike.location = location;
                    hike.distance = distance;
                    hike.rating = rating;
                    hike.comments = comments;
                    break;
                }
            }
        }, 0);
    });
}

거의 다 되었습니다. 마지막으로, hike 오브젝트가 업데이트 된 후에 Promise 를 resolve 할 것입니다. 우리가 만드는 Promise 는 어떤 데이터도 반환하지 않으므로, 매개 변수없이 resolve 만 호출하면 됩니다.:

function updateHike(id, name, location, distance, rating, comments) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            for (var i = 0; i < hikes.length; i++) {
                var hike = hikes[i];
                if (hike.id == id) {
                    hike.name = name;
                    hike.location = location;
                    hike.distance = distance;
                    hike.rating = rating;
                    hike.comments = comments;
                    break;
                }
            }

            resolve();
        }, 0);
    });
}

우리 모의 백엔드의 인터페이스를 위한 것이 거의 다 되었습니다. 마지막으로 해야 할 일은, 다음과 같이 hikes 배열 대신 이 함수를 exports 하는 겁니다.:

module.exports = {
    getHikes: getHikes,
    updateHike: updateHike
};

이것으로 모의 백엔드가 완성되었습니다!

우리의 컨텍스트 추상화

이미 언급했듯이, 우리 뷰 모델은 우리 모의 백엔드와 직접적으로 상호 작용 할 수 있습니다. 기술적으로는 아무 문제가 없지만, 우리 뷰 모델들이 대신 상호 작용하는, 백엔드 위 작은 추상화 레이어를 만드는 것이 더 유리한 측면이 있습니다. 이렇게 하면 모델에 보다 일관된 인터페이스를 제공 할 수 있으며, 캐싱과 같은 기능을 구현함으로써, 앱에서 사용하는 대역폭과 배터리 양을 줄일 수 있게됩니다. 따라서 우리가 할 다음 작업은 Context 라고 하는 추상화를 만드는 것입니다.

Context 모듈을 만들려면 Modules 디렉토리에서 Context.js 라는 새 파일을 만듭니다.:

.
|- MainView.ux
|- Modules
|  |- Backend.js
|  |- Context.js
|- Pages
|  |- EditHikePage.js

...

이 파일에서, FuseJS Observable 모듈을 가져옵니다.

var Observable = require("FuseJS/Observable");

또한 Backend 모듈을 다음과 같이 가져옵니다.

var Observable = require("FuseJS/Observable");
var Backend = require("./Backend");

Backend 모듈을 ./Backend 로 가져온 것을 잘 보십시오. 일반적으로 앱에 번들되는 JS 모듈을 포함하려할 때, 우리는 require 표현식을 사용하여, 해당 모듈을 resolve 할 프로젝트의 루트 디렉토리를 기준으로 한 상대적인 어떤 경로를 지정합니다. 그러나 우리는 마찬가지로 어떤 모듈을 resolve 할 상대 경로들을 사용 할 수도 있습니다. 이 경우, 그게 정확히 우리가 한겁니다. - 우리는 Backend.jsContext.js 파일과 같은 디렉토리인, Modules 디렉토리에 있다는 것을 알고 있습니다. 따라서 ./Backend 를 사용하면 됩니다.

이제 Context 모듈 핵심에 대한 것입니다. Context 는 우리 앱의 데이터를 쉽게 소비하고 수정할 수 있는 간단한 인터페이스로 우리 뷰 모델들을 제공해야 합니다. 우리는 우리 뷰들이 결국 데이터 바인딩을 통해 뷰 모델들의 데이터를 표시할 것을 알고 있으므로, 해당 Context 는 이상적으로 하나 혹은 그 이상의 Observable 들을 통해 우리 데이터를 노출해야 합니다. 그래서, 우리는 간단한 hikes Observable 로 시작할 것입니다 :

var hikes = Observable();

Observable 은 뷰 모델에서 사용 가능한 모든 하이킹들을 표시하기 위해 사용 될 겁니다. 앱이 시작되면 우리는 백엔드의 데이터를 사용하여 이 Observable 을 채 웁니다. 앱이 실행될 때, 이 컬렉션은 본질적으로 백엔드 내 같은 컬렉션의 로컬 미러가 됩니다. 우리가 변경을 하게 되면, 뷰 모델이 즉시 업데이트되도록 이 컬렉션의 내용을 업데이트 할 것입니다. 우리는 또한 백엔드와 통신해서, 해당 정보를 비동기로 업데이트 할 수 있습니다.

우리 앱이 시작되면, 우리는 Backend 모듈의 getHikes 함수를 호출하고, 그것의 초기 데이터로 hikes Observable 을 채우기 위해 반환되는 Promise 를 사용할 것입니다.:

var hikes = Observable();

Backend.getHikes()
    .then(function(newHikes) {
        hikes.replaceAll(newHikes);
    })
    .catch(function(error) {
        console.log("Couldn't get hikes: " + error);
    });

Promise 가 수행될때 우리가 얻는 배열의 내용으로 hike Observable 의 내용을 덮어쓰기 위해, replaceAll 함수를 사용한 것을 보십시오. 작은 에러 핸들러도 추가로 붙였습니다. 이제 시작시 백엔드로부터의 초기 데이터로 우리 hikes Observable 을 채웁니다.

다음으로 데이터를 업데이트하기 위한 뷰 모델들에 대한 메커니즘도 제공해야 합니다. 우리 모의 백엔드와 유사하게, 업데이트 할 하이킹을 식별하는 id 와 업데이트 할 데이터를 취하는 updateHike 함수를 제공 할 수 있습니다. 이 함수는 로컬 hikes Observable (단순하고 일관성있는 것들을 유지하기 위해 백엔드에서 사용했던 것과 유사한 검색을 사용하여) 을 업데이트하고, 백엔드로 하여금 데이터를 업데이트하도록 알립니다.

function updateHike(id, name, location, distance, rating, comments) {
    for (var i = 0; i < hikes.length; i++) {
        var hike = hikes.getAt(i);
        if (hike.id == id) {
            hike.name = name;
            hike.location = location;
            hike.distance = distance;
            hike.rating = rating;
            hike.comments = comments;
            hikes.replaceAt(i, hike);
            break;
        }
    }
    Backend.updateHike(id, name, location, distance, rating, comments)
        .catch(function(error) {
            console.log("Couldn't update hike: " + id);
        });
}

마지막으로, 다음과 같이 모듈의 exports 에서 hikesupdateHike 를 exports (노출) 합니다.:

module.exports = {
    hikes: hikes,

    updateHike: updateHike
};

이로써, 우리는 Context 모듈을 완성했습니다!

모든 것을 연결

이제 BackendContext 모듈들을 설정 했으므로, 이전에 가지고 있던 hikes 모듈 대신 뷰 모델들을 사용해 리팩터링 할 차례입니다.

우리는 HomePage 를 옮기기 시작할 것입니다. 이 페이지에는 이전 hikes 모듈의 하이킹 목록만 표시했었으므로 꽤 간단합니다. 우리는 Pages/HomePage.js 에서 몇 가지 변경만 하면됩니다. 첫째, HomePage.js 를 살펴 보면, 첫 번째 라인은 이전 hikes 모듈을 import 하고 있습니다. 그 대신 context 모듈을 import 하도록 변경합시다.

var Context = require("Modules/Context");

해야 할 또 다른 작업은, 모듈 exports 에서 hikes 대신 Context.hikes 를 참조하도록 변경하는 것 뿐입니다.

module.exports = {
    hikes: Context.hikes,

    goToHike: goToHike
};

그러면 그것은 우리 HomePage 를 커버하게 될 것입니다.

EditHikePage 차례입니다. 이 페이지는 Router 로부터 해당 하이킹 데이터를 받기 때문에, 이 페이지는 이 데이터를 표시하기 위한 어떤 변경도 필요하지 않습니다. 그러나 우리는 실제로 해당 데이터를 편집하기 전에 몇 가지 변경이 필요 할 것입니다. 의미있는 연결을 위해, 마지막 챕터 에서 만든 임시 Back 버튼 대신, 원래 디자인에서의 SaveCancel 버튼을 설정하려고 합니다.

save 버튼부터 시작하겠습니다. 이 버튼은 이전 페이지로 돌아갈 뿐만 아니라, 에디터에서 변경 한 내용을 데이터 모델에 커밋 한다는 점을 제외하곤, 이전에 만든 Back 버튼과 거의 동일합니다. 그러므로 간단하게 기존 Back 버튼 이름을 Save 으로 바꿉니다.

먼저 Pages/EditHikePage.ux 에서 버튼 텍스트와 clicked 핸들러를 모두 변경합니다.:

            <Text>Comments:</Text>
            <TextView Value="{comments}" TextWrapping="Wrap" />

            <Button Text="Save" Clicked="{save}" />
        </StackPanel>

다음으로 Pages/EditHikePage.js 에서 goBack 핸들러의 이름을 save 하도록 바꿉니다.:

function save() {
    router.goBack();
}

그리고 모듈의 exports 에서도 이름을 업데이트 할 것입니다.:

    rating: rating,
    comments: comments,

    save: save
};

마지막으로, 이 버튼은 뷰에서 수행 한 모든 수정을 커밋합니다. 이를 위해, Context 모듈의 updateHike 함수를 호출하여, 우리 hike Observable 들의 값과 우리 뷰 모델의 Observable 들에 포함 된 데이터로 부터 해당 id 를 전달합니다.:

function save() {
    Context.updateHike(hike.value.id, name.value, location.value, distance.value, rating.value, comments.value);
    router.goBack();
}

훌륭합니다! 이제 Save 버튼은 모두 연결되어 있어야 합니다. Cancel 버튼도 구현해 보겠습니다. Cancel 버튼은 우리가 에디터에서 변경한 내용을 어떻게든 취소해야 한다는 점을 제외하면, Save 버튼과 매우 비슷합니다. 그러나 우리가 그 세부 사항에 대한 걱정에 앞서, Cancel 버튼과 해당 cancel 핸들러를 만들어 봅시다.

먼저 Save 버튼에 대해 작성한 코드 바로 아래에 있는, Pages/EditHikePage.ux 안 해당 버튼의 UX 코드를 추가합니다.:

            <Button Text="Save" Clicked="{save}" />
            <Button Text="Cancel" Clicked="{cancel}" />
        </StackPanel>

그런 다음 빈 cancel 함수를 추가하고 Pages/EditHikePage.js 에서 그것을 exports 합니다.:

function cancel() {
}

...

module.exports = {
    ...

    cancel: cancel,
    save: save
};

그런 다음, save 핸들러처럼, Router 인스턴스에서 goBack 을 호출하여 cancel 함수가 이전 페이지로 돌아갈 수 있도록 합니다.:

function cancel() {
    router.goBack();
}

마지막으로, 핸들러가 이전 페이지로 돌아 가기 전에, 뷰 모델에서 변경한 사항들을 되돌리고 싶습니다. 우리가 이 작업을 수행 할 수있는 몇 가지 방법이 있지만, 가장 쉬운 방법 중 하나는, 우리 뷰 모델에 있는 모든 취소 Observable 들이 EditHikePagehike Observablemap 을 통한 결과 라는 점을 이용하는 것입니다. 이 때문에 Observable 의 값을 "새로 고침" 하면, 모든 에디터 값들이 원래 값으로 재설정 됩니다. 이는 다음과 같이 hike Observable 의 값을 그 자체에 할당함으로써 간단히 완성 할 수 있습니다 :

function cancel() {
    hike.value = hike.value;
    router.goBack();
}

쉽습니다! 자, 지금 우리는 이 코드를 이해하고 있지만, 이 코드를 읽지 않은 사람에겐 (또는 나중에 우리가 이 트릭에 대해 잊어 버린 경우) 왜 이렇게 하는지 분명하지 않을 수 있습니다. 그러니 계속 진행하면서 주석을 달아줍시다.:

function cancel() {
    // 의존하는 Observable 들의 값들을 리셋 하기 위해 hike 값을 새로고침 함
    hike.value = hike.value;
    router.goBack();
}

완벽합니다! 이제 모든 것이 연결되어져야 합니다. 이제 우리는 서로 다른 파일들을 모두 저장할 수 있습니다. Fuse가 미리보기를 다시 시작하면, 마침내 우리는 완전한 기능을 가진 뷰들을 시험해 볼 수 있습니다!

지금까지 우리의 진행

결국, 우리는 기본적인 앱의 주요 기능 부분을 모두 갖추고 연결 했습니다. 이제 우리는 완벽히 조화되는 다양한 페이지들과 함께, 멋지고, 확장 가능한 아키텍처와 잘 연동된 모듈들을 구현했습니다. 다음과 같이 보입니다.: https://res.cloudinary.com/fusetools/image/upload/documentation_v2/3e1775cc8b7d386f31a387a5b250e58b__media/hikr/chapter-5/chapter-5.mp4

이 장에서 수정 한 다양한 파일들의 코드는 다음과 같습니다.:

Modules/Backend.js :

var hikes = [
    {
        id: 0,
        name: "Tricky Trails",
        location: "Lakebed, Utah",
        distance: 10.4,
        rating: 4,
        comments: "This hike was nice and hike-like. Glad I didn't bring a bike."
    },
    {
        id: 1,
        name: "Mondo Mountains",
        location: "Black Hills, South Dakota",
        distance: 20.86,
        rating: 3,
        comments: "Not the best, but would probably do again. Note to self: don't forget the sandwiches next time."
    },
    {
        id: 2,
        name: "Pesky Peaks",
        location: "Bergenhagen, Norway",
        distance: 8.2,
        rating: 5,
        comments: "Short but SO sweet!!"
    },
    {
        id: 3,
        name: "Rad Rivers",
        location: "Moriyama, Japan",
        distance: 12.3,
        rating: 4,
        comments: "Took my time with this one. Great view!"
    },
    {
        id: 4,
        name: "Dangerous Dirt",
        location: "Cactus, Arizona",
        distance: 19.34,
        rating: 2,
        comments: "Too long, too hot. Also that snakebite wasn't very fun."
    }
];

function getHikes() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(hikes);
        }, 0);
    });
}

function updateHike(id, name, location, distance, rating, comments) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            for (var i = 0; i < hikes.length; i++) {
                var hike = hikes[i];
                if (hike.id == id) {
                    hike.name = name;
                    hike.location = location;
                    hike.distance = distance;
                    hike.rating = rating;
                    hike.comments = comments;
                    break;
                }
            }

            resolve();
        }, 0);
    });
}

module.exports = {
    getHikes: getHikes,
    updateHike: updateHike
};

Modules/Context.js :

var Observable = require("FuseJS/Observable");
var Backend = require("./Backend");

var hikes = Observable();

Backend.getHikes()
    .then(function(newHikes) {
        hikes.replaceAll(newHikes);
    })
    .catch(function(error) {
        console.log("Couldn't get hikes: " + error);
    });

function updateHike(id, name, location, distance, rating, comments) {
    for (var i = 0; i < hikes.length; i++) {
        var hike = hikes.getAt(i);
        if (hike.id == id) {
            hike.name = name;
            hike.location = location;
            hike.distance = distance;
            hike.rating = rating;
            hike.comments = comments;
            hikes.replaceAt(i, hike);
            break;
        }
    }
    Backend.updateHike(id, name, location, distance, rating, comments)
        .catch(function(error) {
            console.log("Couldn't update hike: " + id);
        });
}

module.exports = {
    hikes: hikes,

    updateHike: updateHike
};

Pages/HomePage.js :

var Context = require("Modules/Context");

function goToHike(arg) {
    var hike = arg.data;
    router.push("editHike", hike);
}

module.exports = {
    hikes: Context.hikes,

    goToHike: goToHike
};

Pages/EditHikePage.ux :

<Page ux:Class="EditHikePage">
    <Router ux:Dependency="router" />

    <JavaScript File="EditHikePage.js" />

    <ScrollView>
        <StackPanel>
            <Text Value="{name}" />

            <Text>Name:</Text>
            <TextBox Value="{name}" />

            <Text>Location:</Text>
            <TextBox Value="{location}" />

            <Text>Distance (km):</Text>
            <TextBox Value="{distance}" InputHint="Decimal" />

            <Text>Rating:</Text>
            <TextBox Value="{rating}" InputHint="Integer" />

            <Text>Comments:</Text>
            <TextView Value="{comments}" TextWrapping="Wrap" />

            <Button Text="Save" Clicked="{save}" />
            <Button Text="Cancel" Clicked="{cancel}" />
        </StackPanel>
    </ScrollView>
</Page>

Pages/EditHikePage.js :

var Context = require("Modules/Context");

var hike = this.Parameter;

var name = hike.map(function(x) { return x.name; });
var location = hike.map(function(x) { return x.location; });
var distance = hike.map(function(x) { return x.distance; });
var rating = hike.map(function(x) { return x.rating; });
var comments = hike.map(function(x) { return x.comments; });

function cancel() {
    // Refresh hike value to reset dependent Observables' values
    hike.value = hike.value;
    router.goBack();
}

function save() {
    Context.updateHike(hike.value.id, name.value, location.value, distance.value, rating.value, comments.value);
    router.goBack();
}

module.exports = {
    name: name,
    location: location,
    distance: distance,
    rating: rating,
    comments: comments,

    cancel: cancel,
    save: save
};

hikr.unoproj :

{
  "RootNamespace":"",
  "Packages": [
    "Fuse",
    "FuseJS"
  ],
  "Includes": [
    "*",
    "Modules/*.js:Bundle"
  ]
}

다음은 뭔가요?

이제 앱의 주요 부분이 모두 갖춰졌으므로, 앱을 돋보이게 만들기 위해 앱의 룩앤필(모양과 느낌) 을 반복적으로 조정해야 할 차례 입니다. 다음 챕터 에서는 커스텀 look/feel 로 다양한 재사용 가능 컴포넌트들을 빌드하여, 앱을 멋지게 꾸미기 위해 그것들을 앱 전체에 적용할 것입니다. 그럼 해 봅시다 !

이 장의 마지막 코드는 여기 에서 볼 수 있습니다.

results matching ""

    No results matching ""