본 번역문서는 개인적인 취지로 번역되어 제공되는 문서로, 원문을 비롯한 모든 저작권은 마이크로소프트사에 있습니다. 마이크로소프트사의 요청이 있을 경우, 언제라도 게시가 중단될 수 있습니다. 본 번역문서에는 오역이 포함되어 있을 수 있으며 주석도 번역자 개인의 견해일뿐입니다. 마이크로소프트사는 본 문서의 번역된 내용에 대해 일체의 보장을 하지 않습니다. 번역이 완료된 뒤에도 제품이 업그레이드 되거나 기능이 변경됨에 따라 원문도 변경되거나 보완되었을 수 있으므로 참고하시기 바랍니다.
원문: https://docs.asp.net/en/latest/client-side/knockout.html
역주 : 시작에 앞서 태오사이트에 이미 Knockout에 대한 소개 강좌가 번역되어 올라온 적이 있었기에 그 내용도 공유합니다. 이번 컬럼을 읽고 Knockout에 관심이 생기신 분들은 다음 링크의
글도 한번 읽어보시면 좋을 듯 합니다.
KnockOut(일명 KO) 시작하기 : 김정훈 번역
Knockout.js MVVM 프레임워크
원문 작성 : Steve Smith
Knockout은 복잡한 데이터 기반의 사용자 인터페이스를 쉽게 만들 수 있도록 하는 대중적인 JavaScript 라이브러리입니다. 이는 단독으로도 사용할 수 있지만 jQuery와 같은 다른 라이브러리와 함께 사용할 수도 있습니다. Knockout의 주요 목적은 JavaScript 개체로 정의된 내부 데이터 모델과 UI 요소들을 바인딩하는 것입니다. 그래서, UI에 변경이 발생하면 모델도 따라서 변경되고요. 그 반대의 경우도 마찬가지입니다. Knockout은 웹 응용프로그램의 클라이언트 측 동작에서 MVVM(Model-View-ViewModel, 모델-뷰-뷰모델) 패턴을 사용할 수 있도록 지원해 주는데요. Knockout의 MVVM 구현을 사용하려는 경우 반드시 알아야 할 두 가지 주요 개념은 Observable과 Binding입니다.
ASP.NET Core에서 Knockout 시작하기
Knockout은 단일 JavaScript 파일로 배포되기 때문에 설치와 사용은 bower를 사용하면 매우 수월합니다. 여러분이 이미 bower와 gulp를 구성해 두었다고 가정하고, ASP.NET Core 프로젝트에서 bower.json을 열고 다음에서 보이는 것과 같이 knockout 종속성을 추가합니다.
{
"name": "KnockoutDemo",
"private": true,
"dependencies": {
"knockout": "^3.3.0"
},
"exportsOverride": {
}
}
이렇게 작성을 한 다음에는 수동으로 bower를 실행해야 합니다. 즉, Task Runner 탐색기(보기 > 다른 윈도우 > Task Runner 관리자)를 열고 Tasks 밑에 있는 bower에 마우스 오른쪽 클릭을 한 다음 Run을 선택합니다. 그 결과는 다음과 같이 보여질 거에요.

만일 지금 프로젝트의 wwwroot
폴더를 살펴 본다면 lib 폴더의 하위로 knockout이 설치된 것이 보일 겁니다.

운영 환경에서는 CDN(Content Delivery Network)를 통해서 knockout을 참조하는 것을 권장합니다. CDN을 사용하면 사용자들이 이미 해당 파일의 캐시된 복사본을 가지고 있어서 아예 다운로드를 할 필요가 없는 상황이 발생할 가능성이 높기 때문입니다. Knockout은 여러 CDN에서 사용이 가능한데요. Microsoft Ajax CDN 경로는 다음과 같습니다.
http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.3.0.js
Knockout을 페이지에 넣어서 사용하려면, 단순히 호스팅할 곳에서 Knockout 파일을 참조하는 <script>
요소를 추가하면 됩니다(직접 참조하든, CDN을 통하든).
<script type="text/javascript" src="knockout-3.3.0.js"></script>
Observable, ViewModel 그리고 단순 바인딩
어쩌면 여러분은 JavaScript를 사용하여 웹 페이지에 있는 요소들을 조작하는 데 익숙할 지도 모릅니다. jQuery와 같은 라이브러리를 사용하거나 또는 직접 DOM으로 접근하는 방식을 사용해서 말이죠. 일반적으로 이러한 방식으로 코드를 작성하는 경우에는 사용자의 특정 액션에 따라서 요소의 값을 직접적으로 설정하는 식으로 구현하곤 합니다. 하지만 Knockout을 사용하면 그 대신 페이지 상의 어떤 요소들과 개체의 속성을 바인드하는 선언적인 방식을 사용할 수 있습니다. 즉, DOM 요소를 조작하는 코드를 작성할 필요가 없으며, 사용자 액션은 단순히 ViewModel 개체와 상호작용하고 Knockout이 페이지 요소들이 동기화되는 것을 책임진다는 것입니다.
간단한 예제로 다음과 같은 페이지 목록을 살펴보죠. 예제에서는 <span>
요소가 data-bind
어트리뷰트를 갖고 있는데, 이 어튜리뷰트는 텍스트 내용이 authorName와 바인드 되어야 한다고 명시하고 있습니다. 그런 다음, JavaScript 블록에서는 viewModel 변수가 단일 속성인 authorName
를 갖도록 정의할 뿐만 아니라 값도 설정하고 있습니다. 마지막으로, ko.applyBindings
를 호출하면서 viewModel 변수를 인자로 넘기고 있죠.
<html>
<head>
<script type="text/javascript" src="lib/knockout/knockout.js"></script>
</head>
<body>
<h1>Some Article</h1>
<p>
By <span data-bind="text: authorName"></span>
</p>
<script type="text/javascript">
var viewModel = {
authorName: 'Steve Smith'
};
ko.applyBindings(viewModel);
</script>
</body>
</html>
브라우저로 결과를 살펴보면 요소의 내용은 viewModel 변수의 값으로 대체되어 있을 겁니다.

이것으로 간단한 단방향 바인딩 작업을 완료했습니다. 어떤 값을 span의 내용으로 할당하기 위해서 JavaScript로 코드를 작성한 것은 아예 없다는 부분에 주목하세요. ViewModel을 사용한다면, 한 단계 더 나아가 HTML input 텍스트박스를 추가하고 값을 바인딩 할 수도 있습니다. 다음과 같이 말이죠.
<p>
Author Name: <input type="text" data-bind="value: authorName" />
</p>
페이지를 새로 고치면, 이 값이 실제로 input 박스에 바인딩된 것을 볼 수 있습니다.

하지만, 텍스트박스에서 값을 변경해도 <span>
요소에서 그에 대응하는 값은 변경되지 않습니다. 왜 그럴까요?
이 문제는 그 누구도 <span>
에게 업데이트가 필요하다고 알려주지 않았기 때문입니다. 사실 ViewModel의 속성이 특별한 형식으로 랩핑되어 있지 않다면, 단지 ViewModel을 업데이트하는 것만으로는 충분하지 않습니다. ViewModel의 어떤 속성이 변경됨에 따라서 자동으로 변경내용이 업데이트되게 해야 한다면 해당 속성에 observables(옵저서블)를 사용할 필요가 있습니다. 즉, 단순하게 "value"를 사용하는 대신 ko.observable("value")
를 사용하도록 ViewModel 을 변경한다면, ViewModel은 변경이 발생할 때마다 그의 값과 바인딩 되어있는 모든 HTML 요소들을 업데이트할 겁니다. 다만, input 상자는 포커스를 잃기 전까지는 값이 업데이트되지 않는다는 점에 주의하세요. 그렇기에, 입력을 함에 따라서 실시간으로 바인딩된 요소가 변경되는 것은 볼 수 없을 겁니다.
노트 : 각각의 키 입력 후에 실시간으로 업데이트되게 만들고 싶다면 data-bind
어트리뷰트의 콘텐트에 valueUpdate: "afterkeydown"
를 추가하면 됩니다.
ko.observable을 사용하도록 변경한 뒤의 viewModel은 다음과 같습니다.
var viewModel = {
authorName: ko.observable('Steve Smith')
};
ko.applyBindings(viewModel);
Knockout은 여러 개의 다양한 바인딩 유형을 지원합니다. 지금까지 살펴본 것은 text
와 value
를 바인딩하는 방법이었는데요. 그 밖에 다른 어트리뷰트들과도 바인딩을 할 수도 있습니다. 예를 들어, 앵커 태그를 갖는 하이퍼링크를 만든다면 src
어트리뷰트를 viewModel과 바인딩할 수 있습니다. Knockout은 또한 함수로의 바인딩도 지원합니다. 이를 확인해 보기 위해서 viewModel을 변경하여 author의 twitter 핸들을 갖도록 하고요. 그 twitter 핸들이 author의 트위터 페이지를 링크하여 출력하게끔 만들어 보겠습니다. 이것을 3단계로 나누어 진행해 볼게요.
우선, 하이퍼링크를 출력하는 HTML을 추가해야 하는데요. author의 이름 뒤 쪽으로 괄호 안에 넣도록 해요.
<h1>Some Article</h1>
<p>
By <span data-bind="text: authorName"></span>
(<a data-bind="attr: { href: twitterUrl}, text: twitterAlias"></a>)
</p>
그 다음, twitterUrl과 twitterAlias 속성을 갖도록 viewModel을 변경합니다.
var viewModel = {
authorName: ko.observable('Steve Smith'),
twitterAlias: ko.observable('@ardalis'),
twitterUrl: ko.computed(function () {
return "https://twitter.com/";
}, this)
};
ko.applyBindings(viewModel);
아직까지는 twitterUrl이 twitter 앨리아스에 맞는 정확한 URL로 가도록 구성되어 있지 않고 그냥 twitter.com을 가리키고 있는 부분은 주목해 주세요. 또한, twitterUrl에 대해서는 computed
라는 새로운 Knockout 함수를 사용하고 있는 부분에도 주목해 주세요. 이는 변경될 경우 모든 UI 요소에 통보를 하는 observable 함수입니다. 하지만, 함수 안에서 viewModel 안에 있는 다른 속성들에 접근하려면, viewModel을 생성하는 방식을 변경해서 각각의 속성이 자체 구문(statement)를 갖도록 변경할 필요가 있습니다.
개정된 viewModel 선언은 다음과 같습니다. 이제 viewModel은 함수로 선언되었고 각각의 속성은 세미콜론으로 끝나는 자체 구문을 갖게 되었습니다. 또한, twitterAlias 속성값에 접근하려면 그를 실행할 필요가 있는데요. 그렇게 하려면 ()를 포함해야 합니다.
function viewModel() {
this.authorName = ko.observable('Steve Smith');
this.twitterAlias = ko.observable('@ardalis');
this.twitterUrl = ko.computed(function () {
return "https://twitter.com/" + this.twitterAlias().replace('@', '');
}, this)
};
ko.applyBindings(viewModel);
이제 브라우저에서 보면 원하던 결과를 얻을 수 있습니다.

또한 Knockout은 특정 UI 요소 이벤트에 대한 바인딩도 지원합니다. 예를 들면, 클릭 이벤트 같은 것을 말이죠. 그렇기에 여러분은 응용프로그램의 viewModel 안에서 UI 요소와 함수를 쉽게 선언적으로 바인딩할 수가 있습니다. 간단한 예를 들자면, 버튼을 추가한 뒤 그 버튼이 클릭되면 author의 twitterAlias가 모두 대문자로 변경되도록 만들 수 있다는 것입니다.
우선, 버튼을 추가하면서 버튼의 click 이벤트에 바인딩을 붙여서 추후 viewModel에 추가할 함수 명을 참조하게 합니다.
<p>
<button data-bind="click: capitalizeTwitterAlias">Capitalize</button>
</p>
그 다음, viewModel에 함수를 추가해서 viewModel의 상태를 변경하게 합니다. 새로운 값을 twitterAlias 속성에 설정하려면 그를 메서드로서 호출하면서 새 값에 전달해야 한다는 점에 주의하세요.
function viewModel() {
this.authorName = ko.observable('Steve Smith');
this.twitterAlias = ko.observable('@ardalis');
this.twitterUrl = ko.computed(function () {
return "https://twitter.com/" + this.twitterAlias().replace('@', '');
}, this);
this.capitalizeTwitterAlias = function () {
var currentValue = this.twitterAlias();
this.twitterAlias(currentValue.toUpperCase());
}
};
ko.applyBindings(viewModel);
코드를 실행하고 버튼을 클릭하면 예상한 대로 출력되는 링크가 변경될 겁니다.

흐름 제어
Knockout은 조건부 작업과 반복 작업을 수행할 수 있는 바인딩도 제공합니다. 반복 작업은 특히나 데이터 목록을 UI 목록이나 메뉴, 그리드, 테이블과 같은 곳에 바인딩하는 경우 대단히 유용합니다. foreach 바인딩은 배열을 반복하면서 적용할 것입니다. 특히, observable 배열과 함께 사용된다면, 이는 배열에 항목들이 추가되거나 삭제될 경우 자동으로 UI 요소들을 업데이트할 겁니다. 모든 요소를 UI 트리에 다시 생성하거나 하지 않고서 말이죠. 다음의 예제는 게임 결과의 observable 배열을 갖는 새로운 viewModel을 사용합니다. 이는 <tbody>
요소에 foreach
바인딩을 사용하여 2개의 컬럼을 갖는 간단한 테이블에 바인딩을 하고 있습니다. <tbody>
안에 있는 각각의 <tr>
요소는 gameResults 컬렉션의 요소들과 바인딩될 것입니다.
<h1>Record</h1>
<table>
<thead>
<tr>
<th>Opponent</th>
<th>Result</th>
</tr>
</thead>
<tbody data-bind="foreach: gameResults">
<tr>
<td data-bind="text:opponent"></td>
<td data-bind="text:result"></td>
</tr>
</tbody>
</table>
<script type="text/javascript">
function GameResult(opponent, result) {
var self = this;
self.opponent = opponent;
self.result = ko.observable(result);
}
function ViewModel() {
var self = this;
self.resultChoices = ["Win", "Loss", "Tie"];
self.gameResults = ko.observableArray([
new GameResult("Brendan", self.resultChoices[0]),
new GameResult("Brendan", self.resultChoices[0]),
new GameResult("Michelle", self.resultChoices[1])
]);
};
ko.applyBindings(new ViewModel);
</script>
이번에는 ViewModel을 대문자 “V”를 사용하고 있는 부분에 주의해야 하는데요. 이는 applyBindings 호출 안에서 “new”를 사용해서 생성하고 있기 때문입니다. 실행할 경우 페이지의 결과는 다음과 같을 겁니다.

observable 컬렉션이 잘 동작하는 지 확인하기 위해서 약간의 기능을 더 추가해 보도록 하겠습니다. 우리는 ViewModel에 또 다른 게임의 결과를 기록하는 기능을 추가할 건데요. 그리고 이러한 함수와 함께 동작할 버튼과 약간의 UI를 추가하려 합니다. 우선, addResult 메서드를 만들어 보아요.
// add this to ViewModel()
self.addResult = function () {
self.gameResults.push(new GameResult("", self.resultChoices[0]));
}
이 메서드를 click
바인딩을 사용해서 버튼과 바인딩합니다.
<button data-bind="click: addResult">Add New Result</button>
브라우저에서 페이지를 열고 버튼을 여러 번 클릭해보면, 매 클릭마다 새로운 테이블 행이 추가되는 것을 볼 수 있을 겁니다.

UI에서 새로운 레코드를 추가하도록 하기 위해서는 몇 가지 방안이 있습니다만 일반적으로는 인라인이나 별도의 폼을 사용하죠. 우리는 간단하게 테이블을 변경하여 즉, 텍스트박스와 드롭다운 목록을 사용하도록 수정해서 전체적으로 편집이 가능하게 만들어 볼까 합니다. 단지 <tr>
요소를 다음과 같이 바꾸면 됩니다.
<tbody data-bind="foreach: gameResults">
<tr>
<td><input data-bind="value:opponent" /></td>
<td><select data-bind="options: $root.resultChoices,
value:result, optionsText: $data"></select></td>
</tr>
</tbody>
$root
는 선택이 가능한 항목들이 노출되어 있는 루트 ViewModel을 참조하고 있는 부분에 주목하세요. $data
는 주어진 컨텍스트 안에 있는 현재 모델의 것을 참조합니다(즉 예제의 경우에는 resultChoices 배열의 개별 요소들을 참조하게 되는데요. 각각의 요소는 단순한 문자열입니다).
이렇게 변경하면, 전체 그리드는 편집이 가능하게 됩니다.

Knockout를 사용하지 않는다면 jQuery를 사용해서 이러한 모든 것을 작성해야 합니다만, 이는 그다지 효율적인 방법은 아닐 것입니다. Knockout은 ViewModel 안에 있는 데이터 항목들이 어떤 UI 요소들과 바인딩 되어 있는지를 추적하고, 추가,삭제 혹은 변경될 필요가 있는 요소들만을 업데이트합니다. DOM을 직접 조작한다거나 혹은 jQuery를 사용해서 이러한 작업을 직접 하려면 엄청난 노력이 필요할 겁니다. 심지어 테이블의 데이터들을 기반으로 해서 통계 결과(예를 들면, win-loss 기록)를 추가적으로 출력하고자 한다면 전체 테이블을 한번 더 루프를 돌면서 HTML 요소들을 파싱해야 할 필요가 있습니다. 하지만, Knockout을 사용한다면 숭패(win-loss) 기록을 출력하는 것이 매우 간단합니다. ViewModel 자체에서 계산을 수행하여 <span>
과 단순한 텍스트 바인딩을 사용해서 그 값을 출력하기만 하면 됩니다.
승패 기록을 위한 문자열을 만들기 위해서는 계산된 observable을 사용해야 합니다. ViewModel 안에 있는 observable 속성을 참조하려면 반드시 함수로 호출해야만 합니다. 그렇지 않으면 observable의 값을 가져올 수가 없을 겁니다(즉, 아래의 코드에서와 같이 gameResults
가 아니라 gameResults()
를 사용해야 합니다)
self.displayRecord = ko.computed(function () {
var wins = self.gameResults().filter(function (value) { return value.result() == "Win"; }).length;
var losses = self.gameResults().filter(function (value) { return value.result() == "Loss"; }).length;
var ties = self.gameResults().filter(function (value) { return value.result() == "Tie"; }).length;
return wins + " - " + losses + " - " + ties;
}, this);
이 함수를 페이지 상단의 <h1>
요소 안에 있는 span과 바인딩을 합니다.
<h1>Record <span data-bind="text: displayRecord"></span></h1>
결과는 다음과 같아요.

행을 추가하거나 특정 행의 결과(Result) 컬럼에서 선택된 요소를 변경하면 화면 상단에서 보여지는 기록이 변경될 겁니다.
값들과 바인딩하는 것 뿐만 아니라 바인딩 안에서는 거의 모든 JavaScript 표현식도 사용할 수 있습니다. 예를 들어 UI 요소가 특정 조건 하에서만 나타나야 한다면(값이 특정 기준치를 넘는 경우 등), 그러한 조건을 바인딩 표현식 안에 논리적으로 설정할 수 있습니다.
<div data-bind="visible: customerValue > 100"></div>
상기 <div>
는 오직 customerValue가 100을 넘는 경우에만 보여질 겁니다.
템플릿
Knockout은 템플릿도 지원합니다. 그렇기에 UI와 동작(behavior)을 쉽게 분리할 수 있으며, 규모가 큰 응용프로그램의 경우에는 요청에 따라서 UI 요소들을 지속적으로 로드할 수도 있습니다. 우리는 기존 예제를 약간 수정하여 각각의 행이 자체 템플릿을 사용하게 할까 합니다. 우선 해당 HTML을 템플릿으로 옮기고요. <tbody>
의 data-bind에 해당 템플릿을 이름으로 지정하면 됩니다.
<tbody data-bind="template: { name: 'rowTemplate', foreach: gameResults }">
</tbody>
<script type="text/html" id="rowTemplate">
<tr>
<td><input data-bind="value:opponent" /></td>
<td><select data-bind="options: $root.resultChoices,
value:result, optionsText: $data"></select></td>
</tr>
</script>
Knockout은 또한 그 밖의 템플릿 엔진들(즉, jQuery.tmpl 라이브러리와 Underscore.js의 템플릿 엔진)도 지원합니다.
구성요소들
구성요소는 UI 코드를 체계화하여 재사용할 수 있도록 하는 것인데요. 일반적으로 UI 코드에서 사용하게 되는 ViewModel 데이터와 함께 구성됩니다. 구성요소를 만들려면 그의 템플릿과 ViewModel을 지정한 다음 이름을 주기만 하면 됩니다. 그리고 ko.components.register()
를 호출하여 완료하면 됩니다. 템플릿과 ViewModel을 인라인으로 정의할 수 있을 뿐만 아니라 require.js와 같은 라이브러리를 사용해서 외부 파일로부터 구성요소를 로드할 수도 있습니다. 그리고 이렇게 할 경우 훨씬 깔끔하고 효율적으로 코드를 관리할 수 있을 것입니다.
API와 통신하기
Knockout은 어떠한 데이터든지 JSON 포맷으로 다룰 수 있습니다. Knockout를 사용해서 데이터를 가져오고 저장하는 보편적인 방법은 jQuery를 사용하는 것인데요. 데이터를 가져오기 위해서는 $.getJSON()
함수를 사용하고 브라우저에서 API 끝점으로 데이터를 전송하기 위해서는 $.post()
메서드를 사용하면 됩니다. 물론, JSON 데이터를 가져오고 보내는 데에 있어 다른 방식들을 선호한다면 Knockout은 그 방식과도 잘 동작할 겁니다.
요약
Knockout은 UI 요소들과 ViewModel에 정의되어 있는 클라이언트 응용프로그램의 현재 상태를 바인딩하기 위한 간단하고도 훌륭한 방법을 제공합니다. Knockout의 바인딩 구문은 수행되어야 할 대상인 HTML 요소에 data-bind 어트리뷰트를 적용하는 방식으로 사용합니다. Knockout은 UI 요소들을 추적하여 오직 영향을 받은 요소들에 대해서만 업데이트를 수행하기에 방대한 데이터 집합도 효율적으로 업데이트하고 렌더할 수 있습니다. 거대한 응용프로그램의 경우에는 필요한 시점에 외부 파일로부터 로드될 수 있는 템플릿과 구성요소를 사용하여 UI 로직을 분산할 수도 있습니다. 글을 작성하는 현재 시점에는 Knockout의 3 버전이 안정적인 JavaScript 라이브러리이며, 이를 활용하여 여러분의 웹 응용프로그램을 풍부한 대화형 클라이언트로 향상시킬 수 있습니다.