오늘은 HTTP 통신 환경에서 사용자 편의성을 높이기 위해 자주 사용되는 localStorage와 Cookie에 대해 학습했다. 이후 이를 바탕으로 연락처를 추가, 수정, 삭제할 수 있는 CRUD 기능을 JavaScript로 직접 구현하고, 데이터를 localStorage에 저장하는 실습까지 진행했다.
평소 JavaScript를 이용한 CRUD 구현이나 localStorage 활용을 많이 해보지 않았기 때문에, 이번 학습 내용을 중심으로 흐름을 정리해보려고 한다.
1. localStorage vs sessionStorage vs cookie
브라우저 저장소인 LocalStorage, SessionStorage, Cookie는 데이터를 사용자 브라우저에 보관한다는 공통점이 있지만,
유효 기간, 저장 용량, 서버 전송 여부에서 큰 차이가 있다.
| 구분 | localStorage | sessionStorage | cookie |
| 유효 기간 | 영구적 (직접 삭제 전까지) | 탭 종료 시 삭제 | 설정된 만료일 전까지 (기본은 브라우저 종료 시) |
| 저장 용량 | 약 5MB ~ 10MB | 약 5MB | 약 4KB (매우 작음) |
| 서버 전송 | 전송되지 않음 | 전송되지 않음 | 매 HTTP 요청마다 자동 전송 |
| 보안 옵션 | 없음 (JS로 접근 가능) | 없음 (JS로 접근 가능) | HttpOnly, Secure 플래그 사용 가능 |

1.1 localStorage
- 특징: 브라우저를 닫거나 컴퓨터를 재부팅해도 데이터가 유지되는 영구 저장소이다. 동일한 도메인(Origin)이라면 모든 탭과 창에서 데이터를 공유한다.
- 활용: 자동 로그인 정보, 다크 모드 설정, 사용자 커스텀 레이아웃 등 반영구적인 설정 데이터
= 사용자 편의 설정 + 민감하지 않은 정보 장기 저장 - 주의: XSS 공격으로 스크립트 탈취가 가능하기 때문에 access token을 localStorage에 저장하는 것을 조심해야 한다.
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme");
console.log(theme);
1.2 sessionStorage
- 특징: 윈도우나 탭을 닫으면 데이터가 즉시 삭제되는 임시 저장소이다. 같은 사이트라도 다른 탭에서 열면 각자 별개의 스토리지를 가진다.
- 활용: 일회성 로그인 정보, 입력 폼의 임시 저장, 특정 페이지 내에서의 상태 유지 등 ⇒ 현재 탭에서만 잠깐 유지할 값
const currentStep = sessionStorage.getItem("step"); // 멀티스텝 폼

1.3 cookie
- 특징: 서버와 클라이언트 간의 지속적인 데이터 교환을 위해 설계되었다. 매번 서버로 전송되므로 트래픽을 유발하며 용량이 매우 작다.
- 보안: HttpOnly 설정을 통해 자바스크립트 접근을 막아 XSS 공격으로부터 비교적 안전하게 인증 정보를 관리할 수 있다.
- 활용: 로그인 세션 관리(Session ID), refresh token, 광고 트래킹, "오늘 하루 보지 않기" 팝업 설정 등

document.cookie = "username=홍길동";
2. 연락처 CRUD + localStorage
자바스크립트를 통해 CRUD 하는 방법을 익히고 localStorage에 데이터를 저장하는 방법을 실습하였다.
데이터를 중심으로 함수가 어떤 순서로 실행이 되고 화면에 보여지는지를 익히려고 노력했다.
2.1 html
<div class="container">
<h2>연락처 관리</h2>
<div id="input-group">
<input type="hidden" id="edit-index" />
<!-- 수정 시 인덱스 저장용 -->
<input id="name" type="text" placeholder="이름" />
<input id="phone" type="text" placeholder="전화번호" />
<button id="save-btn" onclick="saveData()">추가</button>
</div>
<table border="1px solid">
<thead>
<tr>
<th>이름</th>
<th>전화번호</th>
<th>관리</th>
</tr>
</thead>
<tbody id="list-body"></tbody>
</table>
</div>
2.2 초기 데이터 로드
- localStorage에 "contacts"라는 이름으로 저장된 값을 꺼냄
- 문자열이니까 JSON.parse()로 배열로 변환
- 저장된 값이 없으면 null이 나오므로 || [] 로 빈 배열 사용
let contacts = JSON.parse(localStorage.getItem("contacts")) || [];
처음 실행하면,
contacts = [];
이미 저장된 데이터가 있으면,
contacts = [
{ name: "홍길동", phone: "010-1111-2222" },
{ name: "김철수", phone: "010-3333-4444" }
];
2.3 render( )
- 현재 contacts 배열 상태를 화면에 다시 그리는 함수
- 기존 테이블 내용을 비움
- contacts 배열을 순회
- 각 연락처를 <tr>로 만들어 붙임
- 마지막에 localStorage에도 저장
localStorage에 저장하는 코드를 render 함수에서 실행하는 이유는?
render()는 화면을 최신 상태로 다시 그리는 함수이다. 즉, 배열이 바뀐 뒤 항상 호출되기 때문에, 여기서 저장하면 화면 상태와 저장 상태를 같이 맞출 수 있다. 다만 실무적으로는 저장 로직과 렌더링 로직을 분리하기도 한다.
function render() {
const listBody = document.getElementById("list-body");
listBody.innerHTML = "";
contacts.forEach((item, index) => {
listBody.innerHTML += `
<tr>
<td>${item.name}</td>
<td>${item.phone}</td>
<td>
<button class="edit-btn" onclick="editData(${index})">수정</button>
<button class="del-btn" onclick="deleteData(${index})">삭제</button>
</td>
</tr>
`;
});
// localStorage 동기화
localStorage.setItem("contacts", JSON.stringify(contacts)); // json -> string
}
저장 로직과 렌더링 로직을 분리한다면?
지금 render()가 두 가지 일을 하고 있다. 하나의 함수에 하나의 역할만을 수행하도록 분리할 수 있다.
1. 화면 그림
2. localStorage 저장
function saveToStorage() {
localStorage.setItem("contacts", JSON.stringify(contacts));
}
function render() {
const listBody = document.getElementById("list-body");
listBody.innerHTML = "";
contacts.forEach((item, index) => {
listBody.innerHTML += `
<tr>
<td>${item.name}</td>
<td>${item.phone}</td>
<td>
<button id="edit-btn" onclick="editData(${index})">수정</button>
<button id="del-btn" onclick="deleteData(${index})">삭제</button>
</td>
</tr>
`;
});
}
saveToStorage( )는 데이터가 추가, 수정, 삭제됐을 때마다 호출한다.
2.4 saveData( )
- 추가와 수정을 담당하는 함수
왜 추가/수정을 한 함수에서 처리할까?
editIndex === “” 를 기준으로
- 값이 비어 있으면 → 지금은 수정 중이 아님 → 새 데이터 추가
- 값이 있으면 → 몇 번째 데이터를 수정 중인지 알고 있음 → 그 위치를 덮어씀
왜 수정 후 index를 초기화할까?
editIndex = “”; 하지 않을 경우, 다음에 버튼을 눌렀을 때에도 계속 수정 모드로 남아 있게 되기 때문이다.
예를 들어서,
- 1번 연락처 수정 버튼 클릭
- edit-index = 1
- 수정 완료
- 초기화 안 함
- 이제 새 연락처를 넣고 "추가"를 눌렀는데
- 실제로는 1번 데이터를 또 덮어씀
그렇기 때문에 수정이 끝나면 반드시 빈 문자열로 돌려놔야 한다.
function saveData() {
const nameInput = document.getElementById("name");
const phoneInput = document.getElementById("phone");
const editIndex = document.getElementById("edit-index").value;
if (!nameInput.value || !phoneInput.value)
return aler("내용을 입력하세요");
// 데이터 추가
const newData = { name: nameInput.value, phone: phoneInput.value };
if (editIndex === "") {
// input box에 데이터가 없으면(수정 버튼을 누르지 않음)
contacts.push(newData);
} else {
// input box에 데이터가 있으면(수정 버튼을 누름)
contacts[editIndex] = newData;
document.getElementById("edit-index").value = "";
document.getElementById("save-btn").innerText = "추가";
// 수정을 하면 수정 완료로 바뀜 -> 추가로 수정
}
// input 초기화
nameInput.value = "";
phoneInput.value = "";
render(); // 렌더링 위치
}
2.5 editData(index)
- 선택한 연락처의 이름/전화번호를 input에 채움
- hidden input에 현재 수정 중인 index를 저장
- 버튼 글자를 "수정 완료"로 바꿈
// 수정 모드 진입 함수
function editData(index) {
document.getElementById("name").value = contacts[index].name;
document.getElementById("phone").value = contacts[index].phone;
document.getElementById("edit-index").value = index;
document.getElementById("save-btn").innerText = "수정 완료";
}
2.6 deleteData(index)
- 삭제 여부 확인
- splice(index, 1)로 해당 위치의 연락처 1개 삭제
- 다시 렌더링
function deleteData(index) {
if (confirm("Are you sure?")) {
contacts.splice(index, 1);
render();
}
}
결과는 아래에서 확인할 수 있다.
Document
gyagoo.dothome.co.kr
메무아
과제를 처음 받았을 때는 우선 화면 구성부터 시작했다. 태그 간의 부모·자식 관계를 연결하고 화면에 정상적으로 렌더링되는 것까지는 확인했지만, localStorage에 데이터를 저장하고 값이 추가·수정·삭제될 때마다 화면을 다시 그려줘야 한다는 점까지는 미처 생각하지 못했다. 그래서 렌더링 로직을 별도의 함수로 분리해야겠다는 생각까지는 하지 못했다.
이후 강사님의 코드를 따라가며 전체적인 흐름을 파악하려고 노력했다. 그 과정에서 어떤 기능을 함수로 묶어야 유지보수가 쉬운지, 그리고 index를 활용해 수정과 삭제 기능을 어떻게 구현할 수 있는지를 익힐 수 있었다.
처음에는 크게 복잡하지 않은 구조라고 생각했지만, 오늘 실습한 코드는 이후에도 충분히 재사용할 수 있는 기본적인 패턴이라는 생각이 들었다. 그래서 단순히 따라치는 데 그치지 않고, 왜 이렇게 구조를 나누는지까지 충분히 이해하고 넘어가야겠다고 느꼈다.
—————————————————————————————————————————————————————
본 후기는 [한글과컴퓨터x한국생산성본부x스나이퍼팩토리] 한컴 AI 아카데미 (B-log) 리뷰로 작성 되었습니다.
