이번 편의 결과물: 메모 목록 항목에 복사·공유 버튼을 추가하고, 툴바에서 알림 권한을 요청해 자동 저장이 끝날 때마다 시스템 알림을 띄웁니다. · 다루는 개념: navigator.clipboard.writeText, Notification 권한 요청·표시, navigator.share
이 편에서 만드는 파일
notes-app/
├── index.html (~ 알림 켜기 버튼 추가)
└── src/
├── notes.js (~ buildNoteText 추가)
├── render.js (~ 복사·공유 버튼 추가)
└── main.js (~ 클립보드·알림·공유 이벤트 연결)개념 정리
Clipboard API
navigator.clipboard.writeText(text)는 문자열을 시스템 클립보드에 씁니다. Promise를 반환하는 비동기 API이고, HTTPS나 localhost 같은 보안 컨텍스트(secure context)에서만 동작합니다.
Notification API
Notification은 운영체제 알림 센터에 메시지를 띄웁니다. 사용 전에 권한을 요청해야 합니다.
Notification.permission 값 | 의미 |
|---|---|
default | 아직 물어보지 않음 |
granted | 사용자가 허용함 |
denied | 사용자가 거부함(다시 요청해도 브라우저가 자동으로 거부) |
Notification.requestPermission()은 Promise<string>을 반환합니다. denied 상태에서는 코드로 다시 권한을 얻을 수 없고, 사용자가 브라우저 설정에서 직접 바꿔야 합니다.
Web Share API
navigator.share(data)는 운영체제의 공유 시트(메일·메신저 목록)를 띄웁니다. 모든 브라우저가 지원하지는 않으므로 navigator.canShare로 먼저 확인하고, 지원하지 않으면 클립보드 복사 같은 대체 동작을 준비합니다.
| 속성 | 용도 |
|---|---|
data.title | 공유 항목 제목 |
data.text | 본문 텍스트 |
실습
1. src/notes.js에 공유용 텍스트 생성 함수 추가
// src/notes.js 맨 아래에 추가
export function buildNoteText(note) {
return `${note.title}\n${note.body}`;
}2. src/render.js에 복사·공유 버튼 추가
17편의 createNoteItem은 그대로 두고, 버튼 두 개만 덧붙입니다.
// src/render.js (createNoteItem 함수만 수정)
export function createNoteItem(note) {
const li = document.createElement('li');
li.className = 'note-item';
li.dataset.id = note.id;
const title = document.createElement('strong');
title.textContent = note.title;
const tags = document.createElement('span');
tags.className = 'note-tags';
tags.textContent = note.tags.join(', ');
const copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.dataset.action = 'copy';
copyBtn.textContent = '복사';
const shareBtn = document.createElement('button');
shareBtn.type = 'button';
shareBtn.dataset.action = 'share';
shareBtn.textContent = '공유';
li.append(title, tags, copyBtn, shareBtn);
if (note.attachment) {
li.append(createAttachmentThumb(note.attachment));
}
return li;
}3. index.html에 알림 켜기 버튼 추가
<!-- index.html의 #toolbar(10편) 안에 추가 -->
<button id="enable-notifications-btn" type="button">알림 켜기</button>4. src/main.js에서 클립보드·알림·공유 연결
// src/main.js (아래만 추가, 04~17편 초기화는 그대로 유지)
import { buildNoteText } from './notes.js';
import { getAllNotes, putNote } from './storage.js';
const noteListEl = document.querySelector('#note-list');
const enableNotifBtn = document.querySelector('#enable-notifications-btn');
enableNotifBtn.addEventListener('click', async () => {
const permission = await Notification.requestPermission();
enableNotifBtn.textContent = permission === 'granted' ? '알림 켜짐' : '알림 거부됨';
});
function notifySaved(note) {
if (Notification.permission !== 'granted') return;
new Notification('메모 저장됨', { body: note.title });
}
noteListEl.addEventListener('click', async (event) => {
const copyButton = event.target.closest('button[data-action="copy"]');
if (copyButton) {
const id = copyButton.closest('.note-item').dataset.id;
const notes = await getAllNotes();
const note = notes.find((item) => item.id === id);
await navigator.clipboard.writeText(buildNoteText(note));
copyButton.textContent = '복사됨';
setTimeout(() => {
copyButton.textContent = '복사';
}, 1500);
return;
}
const shareButton = event.target.closest('button[data-action="share"]');
if (shareButton) {
const id = shareButton.closest('.note-item').dataset.id;
const notes = await getAllNotes();
const note = notes.find((item) => item.id === id);
const shareData = { title: note.title, text: note.body };
if (navigator.canShare && navigator.canShare(shareData)) {
await navigator.share(shareData);
} else {
await navigator.clipboard.writeText(buildNoteText(note));
}
}
});14편의 runSave 함수 안, animateSavedBadge() 호출 다음 줄에 notifySaved 호출을 추가합니다.
// src/main.js의 runSave 함수 안 (14편 코드에 한 줄만 추가)
async function runSave() {
if (!editingId) return;
const body = getEditorText();
const notes = await getAllNotes();
const note = notes.find((item) => item.id === editingId);
await putNote({ ...note, body, updatedAt: new Date().toISOString() });
saveTimer = null;
animateSavedBadge();
notifySaved(note);
}5. 실행
npm run dev확인
- “알림 켜기”를 누르면 브라우저 권한 팝업이 뜨고, 허용하면 버튼 글자가 “알림 켜짐”으로 바뀝니다.
- 메모를 편집하고 자동 저장이 끝나면 시스템 알림 센터에 “메모 저장됨” 알림이 뜹니다(권한을 허용한 경우).
- 목록 항목의 “복사”를 누르면 버튼 글자가 잠깐 “복사됨”으로 바뀌고, 다른 앱에 붙여넣으면 제목과 내용이 들어갑니다.
- 모바일 브라우저나
navigator.share를 지원하는 환경에서 “공유”를 누르면 운영체제 공유 시트가 뜹니다.
직접 해보기
- 클립보드 복사가 실패했을 때(권한 거부 등)
try/catch로 잡아alert를 띄우는 코드를 추가해봅니다. navigator.share가 없는 브라우저에서는 “공유” 버튼 자체를 숨기도록createNoteItem을 수정해봅니다.
정답 보기
// main.js의 copy 처리 부분 수정
try {
await navigator.clipboard.writeText(buildNoteText(note));
copyButton.textContent = '복사됨';
} catch (error) {
alert('클립보드 복사 권한이 없습니다.');
}// render.js의 createNoteItem 안, li.append 이전
if (navigator.share) {
li.append(shareBtn);
}자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
navigator.clipboard가 undefined | HTTP(비보안) 환경에서 실행 | localhost나 HTTPS에서 테스트 |
| 알림 버튼을 여러 번 눌러도 팝업이 다시 안 뜸 | 이전에 denied를 선택함 | 브라우저 사이트 설정에서 알림 권한을 초기화 |
navigator.share is not a function | 데스크톱 브라우저 일부는 Web Share 미지원 | 호출 전에 if (navigator.share)로 지원 여부 확인 |
| 공유 시트는 떴는데 아무것도 안 보냄 | 사용자가 시트에서 취소함(정상 동작) | share()가 던지는 AbortError를 무시하도록 처리 |
확인 문제
문제 14지선다
navigator.clipboard.writeText가 정상 동작하기 위한 조건은?
문제 24지선다
Notification.permission이 denied 상태일 때 다시 requestPermission을 호출하면?
문제 34지선다
navigator.share를 호출하기 전에 navigator.canShare로 먼저 확인하는 이유는?
문제 44지선다
runSave 함수 끝에서 notifySaved(note)를 호출하는 코드의 동작으로 옳은 것은?
참고 자료
Last updated on