본문으로 건너뛰기

새로고침해도 남게: localStorage

자바스크립트 목차웹 페이지 움직이기레슨 72 / 76

목차

메모리 변수 todos에만 할 일을 담아 두면 브라우저를 새로고침(F5)하거나 탭을 닫았다 열었을 때 처음 상태로 돌아갑니다. 새로고침해도 데이터가 그대로 남게 하려면 브라우저의 작은 저장소인 **localStorage**에 **JSON.stringify**와 **JSON.parse**로 저장하고 꺼내야 합니다.

5단계 중 1단계. save()는 JSON.stringify(todos)로 배열을 JSON 문자열로 바꿔 localStorage의 'todos' 키에 저장합니다.

1저장하기: localStorage.setItem과 JSON.stringify

localStorage는 브라우저가 사이트마다 따로 마련해 둔 키·값 저장소입니다. 가장 중요한 특징은 오직 문자열(글자)만 저장할 수 있다는 점입니다.

따라서 객체 배열 todos를 저장할 때는 8단원 JSON: 객체와 글 사이에서 배운 **JSON.stringify(todos)**로 배열을 JSON 문자열로 바꾼 뒤 **localStorage.setItem(키, 값)**에 넘깁니다.

const todos = [
  { id: 1, text: '우유' },
];

const bad = String(todos);
const good =
  JSON.stringify(todos);
console.log(bad);
console.log(good);
실행 결과
[object Object]
[{"id":1,"text":"우유"}]
  • 만약 JSON.stringify 없이 배열을 그대로 setItem('todos', todos)⁠에 넣으면, 브라우저가 배열을 강제로 문자열로 바꿔 '[object Object]'(또는 문자열 배열이면 '우유 사기,산책하기')로 뭉개 버립니다.
  • JSON.stringify(todos)⁠로 바꿔 넣어야 대괄호와 중괄호 구조가 온전히 문자열로 보존됩니다.
// app.js — 브라우저에서 저장할 때
'use strict';

function save(todos) {
  localStorage.setItem(
    'todos',
    JSON.stringify(todos),
  );
}

2꺼내기: getItem의 null 처리와 JSON.parse

저장해 둔 문자열을 다시 꺼낼 때는 **localStorage.getItem('todos')**를 부릅니다. 이때 사용자가 페이지를 맨 처음 열어서 아직 저장한 적이 없다면 getItem은 **null**을 돌려줍니다.

그래서 널 병합 연산자(??)로 **raw ?? '[]'**를 만들어, null일 때는 빈 배열 글자 '[]'가 JSON.parse에 들어가게 합니다. 또한 저장소의 글자가 실수로 깨져 있을 때 앱이 멈추지 않도록 try / catch로 감쌉니다.

function parseTodos(raw) {
  try {
    return JSON.parse(
      raw ?? '[]',
    );
  } catch {
    return [];
  }
}

이어서 세 가지 값으로 parseTodos를 불러 봅니다.

console.log(parseTodos(null));
console.log(
  parseTodos('[{"id":1}]'),
);
console.log(
  parseTodos('{깨진글자'),
);
실행 결과
[]
[ { id: 1 } ]
[]
  • 첫 방문(raw === null): null ?? '[]'가 '[]'가 되어 빈 배열 []을 돌려줍니다.
  • 저장된 값이 있을 때: JSON 문자열을 실제 배열 [ { id: 1 } ]로 복원합니다.
  • 깨진 문자열일 때: JSON.parse가 던진 SyntaxError를 catch가 잡아 안전하게 빈 배열 []을 돌려줍니다.

브라우저의 app.js에서는 이 함수를 load()로 만들어 앱을 시작할 때 한 번 부릅니다.

// app.js — 브라우저에서 불러올 때
'use strict';

function load() {
  const raw = localStorage
    .getItem('todos');
  try {
    return JSON.parse(
      raw ?? '[]',
    );
  } catch {
    return [];
  }
}

예제

결과를 먼저 생각한 뒤 펼쳐 보세요. 맞혀 보기는 점수에 들지 않습니다.

메모리 객체 store로 localStorage를 흉내 낸 코드입니다. saveItems와 loadItems를 거친 뒤 마지막에 찍히는 두 줄은 무엇일까요?

const store = {};
function saveItems(arr) {
  store.cart =
    JSON.stringify(arr);
}
function loadItems() {
  return JSON.parse(
    store.cart ?? '[]',
  );
}
console.log(loadItems().length);
saveItems(['커피', '쿠키']);
console.log(loadItems()[1]);
결과 보기
실행 결과
0
쿠키
  • 처음 loadItems()를 부를 때는 store.cart가 undefined이므로 '[]'를 읽어 길이 0이 찍힙니다.
  • saveItems(['커피', '쿠키'])⁠로 저장한 뒤 다시 loadItems()를 부르면 복원된 배열의 1번 인덱스인 '쿠키'가 찍힙니다.

자주 틀리는 곳

  1. setItem('todos', todos)⁠처럼 배열이나 객체를 그대로 넣는다: localStorage는 문자열만 받으므로 JSON.stringify를 거치지 않으면 원래 배열로 되돌릴 수 없는 문자열('[object Object]')이 저장됩니다.
  2. 첫 방문 시 getItem이 돌려주는 null을 처리하지 않는다: JSON.parse(null)은 null을 돌려주므로, 이어서 todos.push(...)나 todos.filter(...)를 부를 때 Cannot read properties of null⁠이라는 TypeError가 납니다. JSON.parse(raw ?? '[]')⁠로 기본 배열을 보장합니다.
  3. 배열을 바꾼 뒤 save()를 부르지 않는다: 화면을 다시 그리는 render()와 마찬가지로, todos 배열에 할 일을 추가하거나 삭제할 때마다 save()를 다시 불러야 localStorage의 값도 함께 바뀝니다.

확인 문제

  1. localStorage.setItem에 배열 todos를 넣을 때 왜 JSON.stringify(todos)⁠를 써야 하나요?
정답

localStorage는 문자열만 저장할 수 있어서, 배열을 그대로 넣으면 구조가 깨진 문자열로 바뀌기 때문입니다. JSON.stringify로 JSON 문자열로 바꿔 저장해야 나중에 JSON.parse로 원래 배열을 복원할 수 있습니다.

  1. 아직 한 번도 'todos'를 저장한 적이 없는 브라우저에서 localStorage.getItem('todos')⁠를 실행해 raw에 담으면 raw의 값은 무엇이며, 이를 빈 배열 []로 복원하는 식은 어떻게 쓰나요?
정답

raw는 null이며, JSON.parse(raw ?? '[]')⁠로 쓰면 null일 때 '[]'가 들어가 빈 배열 []로 복원됩니다.

  1. 개발자 도구에서 누군가 localStorage의 'todos' 값을 잘못된 문자열 'abc'로 고쳐 놓았습니다. 앱이 시작될 때 SyntaxError로 멈추지 않고 빈 배열 []로 시작하게 하려면 JSON.parse를 어떤 문법으로 감싸야 하나요?
정답

try 블록 안에서 JSON.parse(raw ?? '[]')⁠를 돌려주고 catch 블록에서 return [];를 돌려주도록 try / catch로 감쌉니다.

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

자바스크립트 목차로