이 단계에서 학습할 내용은 다음과 같습니다.
- 기존 웹 애플리케이션을 Chrome 앱 플랫폼에 맞게 조정하는 방법
- 앱 스크립트 (CSP)를 준수하도록 만드는 방법
- chrome.storage.local을 사용하여 로컬 저장소를 구현하는 방법
이 단계를 완료하는 데 필요한 예상 시간: 20분
이 단계에서 완료할 작업을 미리 보려면 이 페이지 하단으로 이동 ↓하세요.
기존 Todo 앱 가져오기
먼저 공통 벤치마크인 TodoMVC의 기본 자바스크립트 버전을 가져옵니다. 를 프로젝트에 가져올 수 있습니다.
todomvc의 참조 코드 우편번호에 TodoMVC 앱 버전이 포함되어 있습니다. 있습니다. todomvc에서 폴더를 포함하여 모든 파일을 프로젝트 폴더로 복사합니다.
index.html을 바꾸라는 메시지가 표시됩니다. 수락하세요.
이제 애플리케이션 폴더에 다음과 같은 파일 구조가 생깁니다.
파란색으로 강조표시된 파일은 todomvc 폴더에 있습니다.
지금 앱을 새로고침합니다 (마우스 오른쪽 버튼 클릭 > App Reload). 기본 UI가 표시되지만 할 일을 추가할 수 있습니다.
스크립트를 CSP (콘텐츠 보안 정책)를 준수하도록 설정
DevTools 콘솔을 엽니다 (마우스 오른쪽 버튼 클릭 > 요소 검사를 선택한 다음 콘솔 탭 선택). 나 인라인 스크립트 실행을 거부하는 오류가 표시됩니다.
앱이 콘텐츠 보안 정책을 준수하도록 하여 이 오류를 해결하겠습니다. 인코더-디코더 아키텍처를
일반적인 CSP 비준수는 인라인 JavaScript로 인해 발생합니다. 인라인 JavaScript의 예는 다음과 같습니다.
DOM 속성 (예: <button onclick=''>
) 및 콘텐츠가 포함된 <script>
태그로 이벤트 핸들러
을 입력합니다.
해결 방법은 간단합니다. 인라인 콘텐츠를 새 파일로 이동하면 됩니다.
1. index.html 하단에서 인라인 자바스크립트를 삭제하고 대신 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()에 대해 알아보기
todo 항목을 저장하고 검색하려면 작업의 set()
및 get()
메서드에 관해 알아야 합니다.
chrome.storage
API
set() 메서드는 키-값 쌍 객체를 첫 번째 매개변수로 허용합니다. 선택적 콜백 함수는 두 번째 매개변수입니다. 예를 들면 다음과 같습니다.
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
get() 메서드는 전송하려는 데이터 저장소 키에 대해 첫 번째 매개변수(선택사항)를 허용합니다. 반환합니다. 단일 키를 문자열로 전달할 수 있습니다. 여러 개의 키가 문자열 또는 사전 객체입니다.
필수인 두 번째 매개변수는 콜백 함수입니다. 반환된 객체에서 저장된 값에 액세스하기 위해 첫 번째 매개변수에서 요청된 키입니다. 예를 들면 다음과 같습니다.
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
가this
Store
프로토타입. (바인드된 함수에 대한 자세한 내용은 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
생성자 메서드는 모든 기존
todo 항목을 찾습니다. 이 메서드는 먼저 데이터 저장소가 있는지 확인합니다. 그렇지 않은 경우에는
todos
의 빈 배열을 만들어 Datastore에 저장하여 런타임 읽기 오류가 없도록 합니다.
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()
메서드는 모델에서 todo를 읽을 때 사용됩니다. 반환된 결과는
'전체', '활성' 또는 '완료됨'으로 필터링할 수 있습니다.
chrome.storage.local
를 사용하도록 find()
를 변환합니다.
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()
는 모델에서 모든 todo를 가져옵니다. 사용할 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()
메서드에는 문제가 있습니다. 두 가지 비동기 작업 (get 및 set)에 따라 다릅니다.
항상 전체 모놀리식 JSON 스토리지에서
작동합니다 둘 이상의 일괄 업데이트
'모든 할 일을 완료로 표시'와 같이 할 일 목록에 포함된 경우
Read-After-Write 더 적절한 데이터 저장소를 사용했다면 이 문제는 발생하지 않을 것입니다.
IndexedDB와 유사하지만 이 Codelab의 변환 작업을 최소화하려고 합니다.
이를 수정하는 방법에는 여러 가지가 있으므로 이번 기회를 통해 save()
를 다음과 같이 약간 리팩터링합니다.
할 일 ID의 배열을 가져와서 한 번에 업데이트합니다.
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. chrome.storage.local
를 사용하여 모든 localStorage
인스턴스를 변환합니다.
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에서 배열로 toggleComplete()
를 한 번만 호출하도록 toggleAll()
를 업데이트합니다.
할 일을 하나씩 완료한 것으로 표시하는 대신 할 일을 하나씩 나열해 볼 수 있습니다. _filter()
호출도 삭제합니다.
toggleComplete
_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. 이제 단일 todo 또는 todo 배열을 모두 허용하도록 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단계 - 알람 및 알림 추가 »로 이동합니다.