퓨즈[Fusetools] 문서[Docs] 번역

  1. Home
  2. 퓨즈[Fusetools] 문서[Docs] 번역
  3. 튜토리얼
  4. 5. 백엔드 흉내내기

5. 백엔드 흉내내기

소개

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

그러나 이제는 아키텍처를 알고 있으므로 이 문제를 해결할 때입니다. 실제 백엔드처럼 작동하는 모듈 인 모의 백엔드(mock backend)를 구현하면 됩니다. 이는 실제 데이터를 디바이스의 저장소나 데이터베이스에 저장하는 대신 실행중인 앱에 로컬로 저장합니다.

퓨즈로 앱을 제작할 때 이처럼 모의 백엔드를 만드는 것이 꼭 필요한 것은 아닙니다. 예를 들어, 기존 백엔드 솔루션을 선택할 수도 있고, 이를 직접 구현할 수도 있습니다. 그러나 이 튜토리얼은 가능한 한 일반화하기 위한 것이므로 일반적으로 백엔드에 독립적인 방식으로 이러한 개념을 제시하여 특정 백엔드의 세부 사항이 아닌 핵심 개념에 중점을 둘 수 있습니다. 이는 백엔드 솔루션이 실제로 사용되는지 여부에 관계없이 향후 앱을 실제 백엔드에 연결할 때 기대할 수 있는 것을 이해할 수 있도록 적어도 한 번 이상 수행하는 것이 좋습니다.

참고: 이 튜토리얼 시리즈를 완성한 후에는 모의 백엔드를 특정 백엔드 솔루션 및 기타 멋진 기능과 통합하여 다양한 “트랙” 으로 확장할 예정입니다!

또한 모의 백엔드를 직접 사용하는 대신 뷰 모델이 상호 작용할 모의 백엔드 위에 씬 추상화를 만듭니다. 추후에 모의 백엔드 작동 방법을 변경하거나 실제 백엔드로 교체하면 뷰 모델 중 어떤 것을 변경해야 하기 때문입니다. 새로운 백엔드와 상호 작용하도록 추상화를 업데이트 할 수 있으며 이전과 동일한 인터페이스를 계속 제공할 수 있습니다. 객체 캐싱과 같은 모의 백엔드가 가지고 있지 않은 기능을 채워서 이러한 추상화를 활용할 수도 있습니다.

그러나 우리가 무엇인가를 만들기 시작하기 전에 전형적인 백엔드와의 인터페이스가 어떤 것인지를 고려해야 합니다. 자, 그럼 떠나봅시다!

이 챕터의 최종 코드는 여기에서 볼 수 있습니다.

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

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

1
2
3
4
// 아이템 객체의 배열을 반환합니다.
function getItems() { ... }
// 아이템을 업데이트 합니다.
function updateItem(...) { ... }

우리 앱은 이 인터페이스를 매우 직관적으로 사용합니다:

1
2
3
4
// 백엔드에서 아이템 객체 가져 오기
var someItems = getItems();
// 백엔드의 아이템 중 하나를 업데이트 합니다.
updateItem(...);

이것은 충분히 직관적이어야 합니다. 그러나 우리는 백엔드가 처리해야 하는 대부분의(전부는 아닐지라도) 배포에 대한 매우 중요한 세부 사항을 무시했습니다. 우리의 단순한 인터페이스가 이미 데이터를 로컬에 가지고 있는 경우라면 잘 작동할 것입니다. 그러나 데이터가 다른 곳에 있는 서버에 저장된다면 어떻게 될까요? 백엔드 서버(또는 디스크 등)가 요청한 데이터로 응답하기를 기다리면서 코드를 그냥 멈추게 할 수는 없습니다. 우리의 요청이 백엔드에 도착하고 백엔드가 데이터를 다시 전송하도록 요청하는 데는 미정의 시간이 걸릴 수 있습니다. 따라서 데이터 검색 및 업데이트는 비동기적으로 이루어져야 합니다. 그리고 자바스크립트에서 어떤 비동기 작업을 할 때, 그것은 Promise 또는 두개의 가까운 것(two nearby)을 발견할 가능성이 매우 높습니다.

Promise에 대한 MDN의 기사를 바꾸어 말하면 “Promise는 현재 또는 미래에 사용할 수 있는(혹은 전혀 사용하지 않는) 값을 나타냅니다.” 이것은 Promise가 우리에게 줄 수 있는 일에 대한 아주 기본적인 설명이지만, 이미 이 설명에서 우리는 우리의 백엔드와 비동기적으로 통신한다는 유즈 케이스에 부합한다는 것을 알 수 있습니다. Promises를 사용하면 일반적인 백엔드 인터페이스가 다음과 같이 보입니다:

1
2
3
4
// 아이템 개체의 배열을 나타내는 Promise를 반환합니다.
function getItems() { ... }
// 아이템이 백엔드에서 업데이트 될 때 수행될 Promise를 반환합니다.
function updateItem(...) { ... }

이걸로는 뭐가 변했는지 모르겠어요! 물론, 실제로 이러한 함수를 사용하는 것은(그리고 우리 모의 백엔드의 경우 구현하는 것) 약간 다르지만 크게 다르지는 않습니다. 예를 들어 이 인터페이스를 사용하는 코드는 다음과 같습니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 백엔드에서 아이템 객체를 비동기적으로 가져옵니다.
var someItems = [];
getItems()
    .then(function(items) {
        someItems = items;
    })
    .catch(function(error) {
        console.log("Couldn't get items: " + error);
    });

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

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

then 함수를 호출함으로써 Promise가 수행될 때 일어날 일을 기술할 수 있습니다. 우리는 Promise가 충족시키는 값을 선택적으로 받아들이는 다른 함수를 전달하여 이 작업을 수행합니다. 이 경우 백엔드의 아이템입니다. 그러나 이 인수는 선택 사항입니다. 예를 들어 updateItem 함수의 경우 Promise는 업데이트가 완료되어도 값을 반환하지 않습니다. Promise가 실제로 완료되었는지 여부만 고려하므로 이 경우 인수가 사용되지 않습니다.

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

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

이제 여러분 중 일부는 흥미로운 것을 발견했을 것입니다. 그것은 우리가 이미 사용하고 있는 퓨즈의 Observables와 Promises가 비슷하다는 것입니다. Observable은 변경 및 관찰할 수 있는 값을 나타내며 비동기 인터페이스에도 적합합니다. 실제로 우리가 원한다면 우리의 백엔드 인터페이스를 모의하기 위해 Promise 대신 Observables를 사용할 수 있습니다. 의미가 매우 비슷하기 때문입니다. 그리고 퓨즈를 대상으로 하는 백엔드를 제작한다면 백엔드와의 통합이 쉽기 때문에 이것이 권장됩니다! 그러나 퓨즈보다 더 많은 플랫폼을 지원하기 위해 많은 백엔드 솔루션이 구축되었기 때문에 우리에게 Promise를 제공하는 인터페이스와 상호 작용할 가능성이 높습니다. 다행스럽게도 Promises와 Observables 사이의 유사점 때문에이 두 가지 사이의 틈을 메우는 것은 나중에 간단히 할 수 있습니다.

참고: this guide over at MDN에서 Promise에 대한 자세한 내용과 퓨즈가 구체적으로 준수하는 Promise의 구문을 설명하는 A+ Promise 표준을 배울 수 있습니다.

대체로 Promises를 사용하여 전형적인 JS 기반 백엔드 인터페이스가 어떻게 생겼는지에 대해 매우 근접한 결과를 얻었습니다. 이것을 모의 백엔드를 모델링하는 데 사용할 것입니다:

모의 백엔드(mock backend) 구현하기

참고: 우리는 프로젝트와 조금 동떨어진 작업을 할 것입니다. 그래서 잠시동안 컴파일이나 미리보기를 할 수는 없겠지만, 이 챕터가 끝날 때까지만 참아주세요.

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

1
2
3
4
5
6
7
.
|- MainView.ux
|- Modules
|- Pages
|  |- EditHikePage.js

...

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

1
2
3
4
5
6
7
...

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

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

새로운 Backend.js 파일을 살펴보면 단순히 하이킹 배열을 그대로 내보내고 있습니다. 그러나 이것이 우리의 모의 백엔드가 될 것이므로 이전에 논의한 인터페이스와 유사한 인터페이스로 변경해야 합니다. 이것은 다음과 같이 보일 것입니다:

1
2
3
4
// 하이킹 객체의 배열을 나타내는 Promise를 반환합니다.
function getHikes() { ... }
// 하이킹이 백엔드에서 업데이트 될 때 수행될 Promise를 반환합니다.
function updateHike(...) { ... }

먼저 getHikes 함수를 만듭니다. 하이킹(hikes) 배열 아래에 빈 함수부터 만들겠습니다:

1
2
function getHikes() {
}

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

1
2
3
function getHikes() {
    return hikes;
}

하지만 우리는 함수가 Promise를 반환하기 원합니다. 이 Promise는 하이킹이 백엔드에서 가져올 시뮬레이션을 준비할 때 수행됩니다. 따라서 우리의 실제 getHikes 함수는 다음과 같습니다:

1
2
3
4
5
function getHikes() {
    return new Promise(function(resolve, reject) {
        resolve(hikes);
    });
}

이제 하이킹 배열을 반환하는 대신 new Promise를 사용하여 Promise를 만듭니다. Promise 생성자는 수행(fulfill)하거나 거부(reject)하기 위해 호출될 함수를 사용합니다. 이 함수는 resolve와 reject의 두 인수를 취합니다. 이러한 인수는 실제로는 함수 자체입니다. resolve는 우리가 만들고 있는 Promise를 수행하는 함수이며, then 함수를 사용하여 Promise에 핸들러를 연결하면 해당 핸들러가 호출됩니다. 우리 코드에서는 Promise를 하이킹 컬렉션으로 해결하기 위해 호출해야 하는 모든 기능이 있습니다. 오류가 발생하고 catch를 통해 Promise에 첨부된 핸들러가 이후에 호출되면 reject 함수가 대신 호출될 수 있습니다.

처리를 완료하기 위해 JS의 내장 setTimeout 함수를 사용하여 밀리초 단위까지 연기할 수 있습니다. setTimeout은 두 개의 인수를 취합니다. 첫 번째 인수는 미래에 언젠가 호출될 함수이고, 두 번째 함수는 해당 함수를 호출하기 전에 지연되는 시간(밀리 초) 입니다. 예를 들어, 다음 코드는 0.5초 후에 Promise의 resolve를 호출합니다.

1
2
3
4
5
6
7
function getHikes() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(hikes);
        }, 500);
    });
}

이 코드는 우리의 앱이 백엔드에서 데이터가 올 때 기다리는 것을 테스트하고자 할 때 매우 유용합니다. 그러나, 테스트를 빠르게 하려면 지연 시간에 0을 사용하세요:

1
2
3
4
5
6
7
function getHikes() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(hikes);
        }, 0);
    });
}

완벽합니다, 인터페이스와 적절한 시뮬레이션 지연을 가진 멋진 getHikes 함수가 완성되었습니다!

이제 updateHike 함수를 만듭시다. 이것은 모의 백엔드에서 업데이트 할 특정 하이킹에 대한 정보를 취하고 업데이트가 완료되면 완료될 Promise를 반환하는 함수입니다. 우리는 빈 함수로 시작합니다:

1
2
function updateHike() {
}

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

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

이 경우 id 인수를 사용하여 업데이트 할 하이킹을 식별하고 나머지 인수는 해당 하이킹의 해당 필드를 모두 덮어쓰게 됩니다.

다음으로 Promise 생성자와 setTimeout을 getHikes처럼 사용하여 선택적인 시간 지연으로 Promise를 반환합니다:

1
2
3
4
5
6
function updateHike(id, name, location, distance, rating, comments) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
        }, 0);
    });
}

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

1
2
3
4
5
6
7
8
9
10
11
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);
    });
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
    });
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
    });
}

그리고 그것은 우리의 모의 백엔드 인터페이스를 위한 것입니다. 마지막으로 해야할 일은 다음과 같이 하이킹 배열 대신 이러한 함수를 내보내는 것입니다:

1
2
3
4
module.exports = {
    getHikes: getHikes,
    updateHike: updateHike
};

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

컨텍스트 추상화

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

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

1
2
3
4
5
6
7
8
9
.
|- MainView.ux
|- Modules
|  |- Backend.js
|  |- Context.js
|- Pages
|  |- EditHikePage.js

...

이 파일에서 FuseJS의 Observable 모듈을 가져옵니다:

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

또한 백엔드 모듈을 다음과 같이 가져옵니다:

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

Backend 모듈을 ‘./Backend’로 가져온 것에 유의하십시오. 일반적으로 앱에 번들로 제공되는 JS 모듈을 가져오기 위해 require 표현식을 사용하고 프로젝트의 루트 디렉토리의 상대적인 경로를 지정합니다. 그러나 현재 경로의 상대 경로를 사용하여 모듈을 확인할 수도 있습니다. 위와 같이 말이죠. 우리는 Backend.js가 Context.js 파일과 같은 디렉토리 인 Modules 디렉토리에 있음을 알고 있습니다. 따라서 ‘./Backend’를 사용하는건 문제가 없습니다.

이제 Context 모듈을 보죠. 컨텍스트는 우리의 뷰 모델에 간단한 인터페이스를 제공하여 앱의 데이터를 쉽게 소비하고 수정할 수 있도록 해야 합니다. 우리는 뷰에 데이터 바인딩을 통해 뷰 모델의 데이터를 표시할 것이므로 컨텍스트는 하나 이상의 Observables를 통해 데이터를 노출하는 것이 이상적입니다. 그래서, 우리는 간단한 hikes Observable로 시작할 것입니다:

1
var hikes = Observable();

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

우리의 앱이 시작되면, 우리는 Backend 모듈의 getHikes 함수를 호출하고 Promise가 반환하는 hikes Observable을 초기 데이터로 채웁니다:

1
2
3
4
5
6
7
8
9
var hikes = Observable();

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

Promise에서 replaceAll 함수를 사용하여 넘어오는 newHikes 배열로 덮어 씁니다. 또한 적절한 조치를 위해 작은 오류 처리기를 추가했습니다. 시작시 백엔드의 초기 데이터로 hikes Observable을 채웁니다.

다음으로 뷰 모델이 데이터를 업데이트하는 메커니즘을 제공해야 합니다. 모의 백엔드와 마찬가지로, 업데이트 할 하이킹을 식별하는 id와 업데이트 할 데이터를 취하는 updateHike 함수를 제공할 수 있습니다. 이 함수는 로컬 hikes Observable을 업데이트하고 (일을 간단하고 일관성있게 유지하는 데 사용된 백엔드와 비슷한 검색을 사용하여) 백엔드에 데이터를 업데이트하도록 지시합니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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에서 hikes와 updateHike를 제공합니다:

1
2
3
4
5
module.exports = {
    hikes: hikes,

    updateHike: updateHike
};

드디어 Context 모듈을 완성했습니다!

모든 것들을 연결하기

이제 백엔드 및 컨텍스트 모듈을 설정 했으므로 이전에 가지고 있던 하이킹 모듈 대신에 뷰 모델을 사용하여 리팩토링 할 때입니다.

우리는 HomePage를 마이그레이션할 것입니다. 이 페이지에는 이전 하이킹 모듈의 하이킹 목록만 표시되었으므로 매우 간단합니다. 우리는 Pages/HomePage.js에서 몇 가지 변경만 하면 됩니다. 첫째, HomePage.js의 첫 번째 라인은 오래된 하이킹 모듈을 가져옵니다. 그 대신 컨텍스트 모듈을 가져오도록 변경합니다:

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

우리가 해야할 또 다른 유일한 일은 모듈 내보내기에서 Context.hikes를 참조하도록 hikes 참조를 변경하는 것입니다:

1
2
3
4
5
module.exports = {
    hikes: Context.hikes,

    goToHike: goToHike
};

그리고 그것은 HomePage를 커버해야 합니다.

이제 EditHikePage를 보겠습니다. 이 페이지는 라우터에서 하이킹 데이터를 받기 때문에 이 페이지는 이 데이터를 표시하기 위해 어떤 변경도 필요하지 않습니다. 그러나 실제로 데이터를 편집하기 전에 몇 가지 사항을 변경해야 합니다. 그러나 의미있는 일을 하기 위해, 우리는 이전 챕터에서 만든 임시 Back 단추 대신에 원래 디자인의 Save 및 Cancel 버튼을 설정하려고 합니다:

Save 버튼부터 시작하겠습니다. 이 버튼은 이전 페이지로 돌아갈뿐만 아니라 편집기에서 변경한 내용을 데이터 모델에 커밋한다는 점을 제외하고 이전에 만든 Back 버튼과 거의 동일합니다. 이제는 기존의 Back 버튼의 이름을 Save로 바꾸겠습니다.

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

1
2
3
4
5
            <Text>Comments:</Text>
            <TextView Value="{comments}" TextWrapping="Wrap" />

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

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

1
2
3
function save() {
    router.goBack();
}

그리고 모듈 내보내기에서도 이름을 업데이트 할 것입니다:

1
2
3
4
5
    rating: rating,
    comments: comments,

    save: save
};

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

1
2
3
4
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 버튼 아래에 Cancel 버튼에 대한 UX 코드를 추가합니다:

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

그런 다음 cancel 함수를 추가하고 모듈 내보내기에 추가합니다:

1
2
3
4
5
6
7
8
9
10
11
function cancel() {
}

...

module.exports = {
    ...

    cancel: cancel,
    save: save
};

그런 다음 save 핸들러와 마찬가지로 Router 인스턴스에서 goBack을 호출하여 cancel 함수가 이전 페이지로 돌아갈 수 있는지 확인합니다:

1
2
3
function cancel() {
    router.goBack();
}

마지막으로, 핸들러가 이전 페이지로 돌아가기 전에 뷰 모델에서 변경한 내용을 되돌리고 싶습니다. 우리가 이 작업을 수행할 수 있는 몇 가지 방법이 있지만, 가장 쉬운 방법 중 하나는 뷰 모델에 있는 모든 Observables가 EditHikePage의 hike Observable map 함수의 결과라는 점을 이용하는 것입니다. 이 때문에 Observable의 값을 “새로고침, refresh” 하면 모든 편집기 값이 원래 값으로 재설정됩니다. 이는 다음과 같이 hike Observable의 값을 그 자체에 할당함으로써 간단히 해결할 수 있습니다:

1
2
3
4
function cancel() {
    hike.value = hike.value;
    router.goBack();
}

간단하네요! 자, 이 코드는 지금 우리에게는 의미가 있지만, 코드를 읽지 않은 사람(미래의 우리 또는 이 트릭에 대해 잊어 버린 경우)에게는 왜 이것을 하고 있는지 분명하지 않을 수 있습니다. 코멘트를 달아주겠습니다:

1
2
3
4
5
function cancel() {
    // 종속성 옵저버블 값을 재설정하기 위해 hike 값 새로고침
    hike.value = hike.value;
    router.goBack();
}

완벽하네요! 이제 모든 것이 연결되었습니다. 이 시점에서 우리는 서로 다른 파일을 모두 저장할 수 있습니다. 일단 퓨즈가 프리뷰를 갱신하면 우리는 마침내 우리의 완전한 기능의 뷰를 시험해 볼 수 있습니다!

우리의 진행 상황

드디어 우리 앱의 기본적인 주요 기능을 모두 만들었으며 서로 연결되어 있습니다. 우리는 다양한 페이지와 모듈을 완벽하고 조화롭게 연동시켜서 멋지고 확장 가능한 아키텍처를 구현했습니다. 잘 따라했다면 다음과 같이 보입니다:

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

Modules/Backend.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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:

1
2
3
4
5
6
7
8
9
10
11
12
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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:

1
2
3
4
5
6
7
8
9
10
11
{
  "RootNamespace":"",
  "Packages": [
    "Fuse",
    "FuseJS"
  ],
  "Includes": [
    "*",
    "Modules/*.js:Bundle"
  ]
}

다음으로 진행할 일

이제 앱의 주요 부분이 모두 갖춰졌으니 앱을 더 돋보이게 만들기 위해 앱의 룩앤필(look and feel)을 조정해야 할 때입니다. 다음 챕터에서는 사용자 정의 룩앤필로 다양하고 재사용 가능한 컴포넌트를 빌드하고 앱 전체에 적용하여 앱을 멋지게 꾸밀 것입니다. 자, 이동하시죠!

이 챕터의 최종 코드는 여기에서 볼 수 있습니다.

Was this article helpful to you? Yes No

How can we help?