이 단계에서는 다음 내용을 알아봅니다.
- Chrome 앱 플랫폼에 맞게 기존 웹 애플리케이션을 조정하는 방법
- 앱 스크립트를 콘텐츠 보안 정책 (CSP)을 준수하도록 하는 방법
- chrome.storage.local을 사용하여 로컬 저장소를 구현하는 방법
이 단계를 완료하는 데 걸리는 예상 시간: 20분
이 단계에서 완료할 내용을 미리 보려면 이 페이지 하단으로 이동 ↓하세요.
기존 할 일 앱 가져오기
시작으로 일반적인 벤치마크 앱인 TodoMVC의 기본 JavaScript 버전을 프로젝트에 가져옵니다.
todomvc 폴더의 참조 코드 zip에 TodoMVC 앱 버전이 포함되어 있습니다. todomvc의 모든 파일 (폴더 포함)을 프로젝트 폴더에 복사합니다.
index.html을 교체하라는 메시지가 표시됩니다. 수락하세요.
이제 애플리케이션 폴더에 다음과 같은 파일 구조가 생겼습니다.
파란색으로 강조 표시된 파일은 todomvc 폴더의 파일입니다.
앱을 새로고침합니다 (마우스 오른쪽 버튼 클릭 > 앱 새로고침). 기본 UI가 표시되지만 할 일을 추가할 수는 없습니다.
스크립트를 콘텐츠 보안 정책 (CSP)을 준수하도록 만들기
DevTools 콘솔을 엽니다 (마우스 오른쪽 버튼 클릭 > 요소 검사를 선택한 다음 콘솔 탭 선택). 인라인 스크립트 실행을 거부하는 오류가 표시됩니다.
앱이 콘텐츠 보안 정책을 준수하도록 하여 이 오류를 수정해 보겠습니다. 가장 일반적인 CSP 비준수 중 하나는 인라인 JavaScript입니다. 인라인 JavaScript의 예로는 DOM 속성 (예: <button onclick=''>
)인 이벤트 핸들러 및 HTML 내부에 콘텐츠가 있는 <script>
태그가 있습니다.
해결 방법은 간단합니다. 인라인 콘텐츠를 새 파일로 이동하면 됩니다.
1. index.html 하단에서 인라인 JavaScript를 삭제하고 대신 js/bootstrap.js를 포함합니다.
<script src="bower_components/director/build/director.js"></script>
<script>
// Bootstrap app data
window.app = {};
</script>
<script src="js/bootstrap.js"></script>
<script src="js/helpers.js"></script>
<script src="js/store.js"></script>
2. js 폴더에 bootstrap.js라는 파일을 만듭니다. 이전에 인라인으로 작성된 코드를 이 파일로 이동합니다.
// Bootstrap app data
window.app = {};
지금 앱을 새로고침하지만 조금만 더 하면 작동하지 않는 Todo 앱이 계속 표시됩니다.
localStorage를 chrome.storage.local로 변환
이제 DevTools 콘솔을 열면 이전 오류가 사라집니다. 그러나 window.localStorage
를 사용할 수 없는 것과 관련된 새로운 오류가 있습니다.
localStorage
은 동기식이므로 Chrome 앱은 localStorage
을 지원하지 않습니다. 단일 스레드 런타임에서 차단 리소스 (I/O)에 동기식으로 액세스하면 앱이 응답하지 않을 수 있습니다.
Chrome 앱에는 객체를 비동기식으로 저장할 수 있는 상응하는 API가 있습니다. 이렇게 하면 때때로 비용이 많이 드는 객체 -> 문자열 -> 객체 직렬화 프로세스를 피할 수 있습니다.
앱의 오류 메시지를 해결하려면 localStorage
를 chrome.storage.local로 변환해야 합니다.
앱 권한 업데이트
chrome.storage.local
를 사용하려면 storage
권한을 요청해야 합니다. manifest.json에서 permissions
배열에 "storage"
를 추가합니다.
"permissions": ["storage"],
local.storage.set() 및 local.storage.get()에 대해 알아보기
할 일 항목을 저장하고 검색하려면 chrome.storage
API의 set()
및 get()
메서드에 관해 알아야 합니다.
set() 메서드는 키-값 쌍의 객체를 첫 번째 매개변수로 허용합니다. 선택적 콜백 함수는 두 번째 매개변수입니다. 예를 들면 다음과 같습니다.
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
get() 메서드는 가져오려는 Datastore 키의 첫 번째 매개변수(선택사항)를 허용합니다. 단일 키는 문자열로 전달할 수 있습니다. 여러 키는 문자열 배열 또는 사전 객체로 정렬할 수 있습니다.
필수인 두 번째 매개변수는 콜백 함수입니다. 반환된 객체에서 첫 번째 매개변수에 요청된 키를 사용하여 저장된 값에 액세스합니다. 예를 들면 다음과 같습니다.
chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});
현재 chrome.storage.local
에 있는 모든 항목을 get()
하려면 첫 번째 매개변수를 생략합니다.
chrome.storage.local.get(function(data) {
console.log(data);
});
localStorage
와 달리 DevTools 리소스 패널을 사용하여 로컬에 저장된 항목을 검사할 수 없습니다. 하지만 다음과 같이 JavaScript 콘솔에서 chrome.storage
와 상호작용할 수 있습니다.
필수 API 변경사항 미리보기
Todo 앱을 변환하는 나머지 단계는 대부분 API 호출을 약간 변경하는 것입니다. localStorage
가 현재 사용 중인 모든 위치를 변경해야 하지만 시간이 오래 걸리고 오류가 발생하기 쉽습니다.
localStorage
와 chrome.storage
의 주요 차이점은 chrome.storage
의 비동기 특성에서 비롯됩니다.
간단한 할당을 사용하여
localStorage
에 쓰는 대신 선택적 콜백과 함께chrome.storage.local.set()
를 사용해야 합니다.var data = { todos: [] }; localStorage[dbName] = JSON.stringify(data);
대
var storage = {}; storage[dbName] = { todos: [] }; chrome.storage.local.set( storage, function() { // optional callback });
localStorage[myStorageName]
에 직접 액세스하는 대신chrome.storage.local.get(myStorageName,function(storage){...})
를 사용한 다음 콜백 함수에서 반환된storage
객체를 파싱해야 합니다.var todos = JSON.parse(localStorage[dbName]).todos;
대
chrome.storage.local.get(dbName, function(storage) { var todos = storage[dbName].todos; });
.bind(this)
함수는this
가Store
프로토타입의this
를 참조하도록 모든 콜백에서 사용됩니다. 결합된 함수에 관한 자세한 내용은 MDN 문서(Function.prototype.bind())를 참고하세요.function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'undefined' }); } new Store();
대
function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'inside Store' }.bind(this)); } new Store();
다음 섹션에서 할 일 항목을 검색, 저장, 삭제하는 방법을 설명할 때 이러한 주요 차이점을 염두에 두세요.
할 일 항목 검색
todo 항목을 가져오기 위해 Todo 앱을 업데이트해 보겠습니다.
1. Store
생성자 메서드는 Datastore의 모든 기존 할 일 항목으로 Todo 앱을 초기화합니다. 이 메서드는 먼저 데이터 스토어가 있는지 확인합니다. 그렇지 않으면 todos
의 빈 배열을 만들어 런타임 읽기 오류가 없도록 데이터 스토어에 저장합니다.
js/store.js에서 생성자 메서드의 localStorage
사용을 대신 chrome.storage.local
를 사용하도록 변환합니다.
function Store(name, callback) {
var data;
var dbName;
callback = callback || function () {};
dbName = this._dbName = name;
if (!localStorage[dbName]) {
data = {
todos: []
};
localStorage[dbName] = JSON.stringify(data);
}
callback.call(this, JSON.parse(localStorage[dbName]));
chrome.storage.local.get(dbName, function(storage) {
if ( dbName in storage ) {
callback.call(this, storage[dbName].todos);
} else {
storage = {};
storage[dbName] = { todos: [] };
chrome.storage.local.set( storage, function() {
callback.call(this, storage[dbName].todos);
}.bind(this));
}
}.bind(this));
}
2. find()
메서드는 모델에서 할 일을 읽을 때 사용됩니다. 반환되는 결과는 '전체', '활성', '완료됨' 중 무엇을 기준으로 필터링하는지에 따라 달라집니다.
find()
를 변환하여 chrome.storage.local
를 사용합니다.
Store.prototype.find = function (query, callback) {
if (!callback) {
return;
}
var todos = JSON.parse(localStorage[this._dbName]).todos;
callback.call(this, todos.filter(function (todo) {
chrome.storage.local.get(this._dbName, function(storage) {
var todos = storage[this._dbName].todos.filter(function (todo) {
for (var q in query) {
return query[q] === todo[q];
}
});
callback.call(this, todos);
}.bind(this));
}));
};
3. find()
와 마찬가지로 findAll()
는 모델에서 모든 할 일을 가져옵니다. findAll()
를 변환하여 chrome.storage.local
를 사용합니다.
Store.prototype.findAll = function (callback) {
callback = callback || function () {};
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
chrome.storage.local.get(this._dbName, function(storage) {
var todos = storage[this._dbName] && storage[this._dbName].todos || [];
callback.call(this, todos);
}.bind(this));
};
할 일 항목 저장
현재 save()
메서드에는 문제가 있습니다. 매번 전체 모놀리식 JSON 저장소에서 작동하는 두 개의 비동기 작업 (get 및 set)에 종속됩니다. '모든 todos를 완료로 표시'와 같이 두 개 이상의 할 일 항목에서 일괄 업데이트를 하면 Read-After-Write라는 데이터 위험이 발생합니다. IndexedDB와 같이 더 적절한 데이터 저장소를 사용하면 이 문제가 발생하지 않지만 이 Codelab에서는 전환 작업을 최소화하려고 합니다.
이를 해결하는 방법에는 여러 가지가 있으므로 이번 기회를 통해 한 번에 업데이트할 todo ID의 배열을 가져와 save()
를 약간 리팩터링합니다.
1. 먼저 save()
내에 있는 모든 것을 chrome.storage.local.get()
콜백으로 래핑합니다.
Store.prototype.save = function (id, updateData, callback) {
chrome.storage.local.get(this._dbName, function(storage) {
var data = JSON.parse(localStorage[this._dbName]);
// ...
if (typeof id !== 'object') {
// ...
}else {
// ...
}
}.bind(this));
};
2. 모든 localStorage
인스턴스를 chrome.storage.local
로 변환합니다.
Store.prototype.save = function (id, updateData, callback) {
chrome.storage.local.get(this._dbName, function(storage) {
var data = JSON.parse(localStorage[this._dbName]);
var data = storage[this._dbName];
var todos = data.todos;
callback = callback || function () {};
// If an ID was actually given, find the item and update each property
if ( typeof id !== 'object' ) {
// ...
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
chrome.storage.local.set(storage, function() {
chrome.storage.local.get(this._dbName, function(storage) {
callback.call(this, storage[this._dbName].todos);
}.bind(this));
}.bind(this));
} else {
callback = updateData;
updateData = id;
// Generate an ID
updateData.id = new Date().getTime();
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, [updateData]);
chrome.storage.local.set(storage, function() {
callback.call(this, [updateData]);
}.bind(this));
}
}.bind(this));
};
3. 그런 다음 단일 항목이 아닌 배열에서 작동하도록 로직을 업데이트합니다.
Store.prototype.save = function (id, updateData, callback) {
chrome.storage.local.get(this._dbName, function(storage) {
var data = storage[this._dbName];
var todos = data.todos;
callback = callback || function () {};
// If an ID was actually given, find the item and update each property
if ( typeof id !== 'object' || Array.isArray(id) ) {
var ids = [].concat( id );
ids.forEach(function(id) {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
for (var x in updateData) {
todos[i][x] = updateData[x];
}
}
}
});
chrome.storage.local.set(storage, function() {
chrome.storage.local.get(this._dbName, function(storage) {
callback.call(this, storage[this._dbName].todos);
}.bind(this));
}.bind(this));
} else {
callback = updateData;
updateData = id;
// Generate an ID
updateData.id = new Date().getTime();
todos.push(updateData);
chrome.storage.local.set(storage, function() {
callback.call(this, [updateData]);
}.bind(this));
}
}.bind(this));
};
할 일 항목을 완료로 표시
이제 앱이 배열에서 작동하므로 완료된 항목 지우기 (#) 버튼을 클릭하는 사용자를 앱이 처리하는 방식을 변경해야 합니다.
1. controller.js에서 toggleAll()
를 업데이트하여 할 일을 하나씩 완료로 표시하는 대신 할 일 배열을 사용하여 toggleComplete()
를 한 번만 호출합니다. 또한 toggleComplete
_filter()
를 조정하므로 _filter()
호출을 삭제합니다.
Controller.prototype.toggleAll = function (e) {
var completed = e.target.checked ? 1 : 0;
var query = 0;
if (completed === 0) {
query = 1;
}
this.model.read({ completed: query }, function (data) {
var ids = [];
data.forEach(function (item) {
this.toggleComplete(item.id, e.target, true);
ids.push(item.id);
}.bind(this));
this.toggleComplete(ids, e.target, false);
}.bind(this));
this._filter();
};
2. 이제 단일 할 일 또는 할 일 배열을 모두 허용하도록 toggleComplete()
를 업데이트합니다. 여기에는 filter()
를 외부가 아닌 update()
내부에 있도록 이동하는 것이 포함됩니다.
Controller.prototype.toggleComplete = function (ids, checkbox, silent) {
var completed = checkbox.checked ? 1 : 0;
this.model.update(ids, { completed: completed }, function () {
if ( ids.constructor != Array ) {
ids = [ ids ];
}
ids.forEach( function(id) {
var listItem = $$('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className = completed ? 'completed' : '';
// In case it was toggled from an event and not by clicking the checkbox
listItem.querySelector('input').checked = completed;
});
if (!silent) {
this._filter();
}
}.bind(this));
};
Count todo items
After switching to async storage, there is a minor bug that shows up when getting the number of todos. You'll need to wrap the count operation in a callback function:
1. In model.js, update getCount()
to accept a callback:
Model.prototype.getCount = function (callback) {
var todos = {
active: 0,
completed: 0,
total: 0
};
this.storage.findAll(function (data) {
data.each(function (todo) {
if (todo.completed === 1) {
todos.completed++;
} else {
todos.active++;
}
todos.total++;
});
if (callback) callback(todos);
});
return todos;
};
2. Back in controller.js, update _updateCount()
to use the async getCount()
you edited in
the previous step:
Controller.prototype._updateCount = function () {
var todos = this.model.getCount();
this.model.getCount(function(todos) {
this.$todoItemCounter.innerHTML = this.view.itemCounter(todos.active);
this.$clearCompleted.innerHTML = this.view.clearCompletedButton(todos.completed);
this.$clearCompleted.style.display = todos.completed > 0 ? 'block' : 'none';
this.$toggleAll.checked = todos.completed === todos.total;
this._toggleFrame(todos);
}.bind(this));
};
You are almost there! If you reload the app now, you will be able to insert new todos without any console errors.
Remove todos items
Now that the app can save todo items, you're close to being done! You still get errors when you attempt to remove todo items:
1. In store.js, convert all the localStorage
instances to use chrome.storage.local
:
a) To start off, wrap everything already inside remove()
with a get()
callback:
Store.prototype.remove = function (id, callback) {
chrome.storage.local.get(this._dbName, function(storage) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}.bind(this));
};
b) Then convert the contents within the get()
callback:
Store.prototype.remove = function (id, callback) {
chrome.storage.local.get(this._dbName, function(storage) {
var data = JSON.parse(localStorage[this._dbName]);
var data = storage[this._dbName];
var todos = data.todos;
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
chrome.storage.local.set(storage, function() {
callback.call(this, todos);
}.bind(this));
}.bind(this));
};
2. The same Read-After-Write data hazard issue previously present in the save()
method is also
present when removing items so you will need to update a few more places to allow for batch
operations on a list of todo IDs.
a) Still in store.js, update remove()
:
Store.prototype.remove = function (id, callback) {
chrome.storage.local.get(this._dbName, function(storage) {
var data = storage[this._dbName];
var todos = data.todos;
var ids = [].concat(id);
ids.forEach( function(id) {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
});
chrome.storage.local.set(storage, function() {
callback.call(this, todos);
}.bind(this));
}.bind(this));
};
b) In controller.js, change removeCompletedItems()
to make it call removeItem()
on all IDs
at once:
Controller.prototype.removeCompletedItems = function () {
this.model.read({ completed: 1 }, function (data) {
var ids = [];
data.forEach(function (item) {
this.removeItem(item.id);
ids.push(item.id);
}.bind(this));
this.removeItem(ids);
}.bind(this));
this._filter();
};
c) Finally, still in controller.js, change the removeItem()
to support removing multiple items
from the DOM at once, and move the _filter()
call to be inside the callback:
Controller.prototype.removeItem = function (id) {
this.model.remove(id, function () {
var ids = [].concat(id);
ids.forEach( function(id) {
this.$todoList.removeChild($$('[data-id="' + id + '"]'));
}.bind(this));
this._filter();
}.bind(this));
this._filter();
};
할 일 항목 모두 삭제
store.js에는 localStorage
를 사용하는 메서드가 하나 더 있습니다.
Store.prototype.drop = function (callback) {
localStorage[this._dbName] = JSON.stringify({todos: []});
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
이 메서드는 현재 앱에서 호출되지 않으므로 추가 과제를 원한다면 직접 구현해 보세요. 힌트: chrome.storage.local.clear()
을 살펴보세요.
완성된 할 일 앱 실행
2단계가 완료되었습니다. 앱을 새로고침하면 이제 완전히 작동하는 Chrome 패키징된 버전의 TodoMVC가 표시됩니다.
추가 정보
이 단계에서 소개된 일부 API에 관한 자세한 내용은 다음을 참고하세요.
- 콘텐츠 보안 정책 ↑
- 권한 선언 ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
다음 단계로 진행할 준비가 되셨나요? 3단계 - 알람 및 알림 추가 »로 이동합니다.