Client Side에서 데이터 저장하기 (Cookie, Web Storage)

나혜수·2023년 2월 8일
0

자바스크립트 실전

목록 보기
6/19

HTTP Cookie

쿠키의 개념

브라우저 쿠키, 웹 쿠키라고도 불리는 HTTP Cookie는 기본적으로 HTTP 통신을 기반으로 하며, 브라우저에서 돌아가는 웹 사이트나 웹 애플리케이션에서 널리 사용되고 있다.

쿠키는 서버가 어떤 데이터를 브라우저 (클라이언트) 측에 저장한 후 다시 그 데이터를 받아오는 기술, 또는 그 데이터 자체를 뜻한다. 쿠키는 HTTP 헤더를 통해 송수신되도록 약속되어 있으며, 다른 저장 방법에 비해 가장 오래된 방식이다.

쿠키의 필요성
그냥 데이터를 모두 서버에 저장해두면 훨씬 편할텐데 뭐하러 서버는 이렇게 수고를 들여 데이터를 굳이 브라우저에 저장하려고 하는가?
→ 요즘처럼 서버 장비의 가격이 저렴하고 심지어 클라우드 인프라까지 이용할 수 있는 환경에서는 이해가 어렵겠지만, 서버 측에서 많은 데이터를 저장하는 것이 부담스러울 정도로 하드웨어의 가격이 비싸던 시절로 돌아가보자.
예를 들어 사용자 한 명당 저장해야할 데이터가 1KB라고 가정하면, 사용자 백만명에게 서비스하기 위해선 서버에 1GB의 저장 공간이 필요하다. 지금이야 일반 노트북에도 수백 GB의 하드디스크가 장착되어 있지만, 인터넷이 태동한 90년대에 이러한 데이터를 서버가 오로지 감당하기는 벅찼을 것이다.
예전에는 이러한 제약사항 때문에 클라이언트 측에 데이터를 저장할 요구가 컸고, 자연스럽게 브라우저에 데이터를 저장하기 위한 쿠키🍪가 고안된 것이다.


쿠키의 매커니즘

쿠키를 기술적으로 제대로 이해하기 위해선 브라우저와 서버가 어떤 과정을 거쳐 쿠키를 주고 받으며 HTTP 통신을 하는지 알아야 한다.

먼저 쿠키라는 데이터는 어떤 모양일까? 쿠키는 <key>=<value> 형태의 단순한 문자열이다. 서버와 브라우저는 HTTP 헤더 안에 이 쿠키를 담아서 주고 받게 된다.

서버가 어떤 쿠키를 브라우저에 저장하고 싶다면 당연히 해당 쿠키를 브라우저에 보내줘야 한다. 다만, 클라이언트 서버 모델에서 클라이언트 요청없이는 서버가 클라이언트로 데이터를 보낼 수 없다. 따라서 이러한 쿠키 전달 과정은 서버가 클라이언트 요청에 응답할 때 일어나게 된다. 서버는 "Set-Cookie" 라는 Response Headers에 브라우저가 수신해야 할 쿠키 정보를 명시한다.

Response Headers
  Set-Cookie: <이름>=<>


이렇게 서버로부터 쿠키를 응답받은 브라우저는 우선 해당 쿠키를 클라이언트 컴퓨터의 하드디스크에 저장한다. 그리고 브라우저가 동일한 서버에 요청을 할 때 저장해놓은 쿠키를 "Cookie" 라는 Request Headers에 실어서 돌려 보낸다. Cookie Request Headers에는 여러 개의 쿠키를 ;로 구분하여 나열할 수 있다.


중요한 점은 서버가 Set-Cookie Response Headers를 통해 브라우저로 쿠키를 보내는 것은 일회성 작업이지만, 반대로 브라우저가 Cookie Request Headers를 통해 서버로 쿠키를 돌려보내는 것은 일정 시간동안 반복해서 수행되는 작업이라는 것이다. 🍪🚚

쿠키의 송수신 예제

예를 들어 서버에서 a=1, b=2 라는 2개의 쿠키를 브라우저에 저장하고 싶다고 가정하자. www.test.com 서버는 브라우저의 요청이 들어오면 각 쿠키를 Set-Cookie 헤더에 실어서 응답한다.

HTTP요청
  GET /index.html HTTP/1.1
  Host: www.test.com
  
HTTP응답
  HTTP/1.1 200 OK
  Content-Type: text/html
  Set-Cookie: a=1
  Set-Cookie: b=2  

브라우저는 서버로부터 받은 이 2개의 쿠키를 클라이언트의 하드디스크에 저장해둔다. 그리고 추후 동일한 서버로 요청을 할 때마다 Cookie 헤더에 이전에 서버로 부터 받았던 쿠키를 그대로 돌려보낸다.

HTTP요청
  GET /index.html HTTP/1.1
  Host: www.test.com
  Cookie: a=1; b=2

해당 브라우저는 사용자가 www.test.com 이라는 도메인에 머무는 한 /index.html을 방문하든 /about.html을 방문하든 /contact.html을 방문하든 매번 같은 쿠키를 돌려보낸다.

HTTP요청
  GET /about.html HTTP/1.1
  Host: www.test.com
  Cookie: a=1; b=2
  
HTTP요청
  GET /contact.html HTTP/1.1
  Host: www.test.com
  Cookie: a=1; b=2

이제 서버는 브라우저로부터 들어오는 요청에서 a=1, b=2라는 쿠키를 읽어서 원하는 용도로 활용할 수 있다.

쿠키의 유효 기간

위에서 브라우저는 일정 시간동안 계속해서 서버로 쿠키를 돌려보낸다고 설명했다. 브라우저가 쿠키를 얼마나 오래동안 돌려보내야 하는지는 서버가 맨 처음 쿠키를 보낼 때 결정하며, Set-Cookie Response Headers를 통해 명시되야 힌다.

유효 기간이 별도로 명시되지 않은 쿠키를 세션 쿠키 (session cookie)라고 부르는데 브라우저의 세션이 종료될 때 함께 만료된다. 즉, 브라우저의 탭을 닫으면 서버가 보냈던 쿠키는 모두 만료되어 브라우저는 더 이상 해당 쿠키를 서버에 돌려보내지 않는다.

반면에 유효 기간이 명시되어 있는 쿠키인 영속 쿠키 (permanent cookie)는 세션과 무방하게 특정 기간이나 특정 시점까지 유효하다. 쿠키의 유효 기간을 명시할 때는 Set-Cookie Response Headers에 Expires, Max-Age 속성을 다음과 같은 형태로 사용한다.

HTTP응답
  Set-Cookie: <쿠키 이름>=<쿠키 >; Expires=종료 시점
  Set-Cookie: <쿠키 이름>=<쿠키 >; Max-Age=유효 기간

Max-Age 속성은 초 단위로 설정하며, Expires, Max-Age 속성이 둘 다 있을 경우 Max-Age 속성이 우선시 된다. 두 가지 속성이 모두 없을 때 해당 쿠키는 세션이 종료될 때 만료되는 세션 쿠키가 된다.

쿠키의 적용범위

브라우저는 기본적으로 쿠키를 보낸 서버가 속한 도메인으로만 쿠키를 되돌려 보내지만, 어떤 URL을 방문할 때 해당 쿠키를 보내야할지 말지를 좀 더 세밀하게 제어할 수도 있다.

  • Set-Cookie Response Headers에 Domain 속성을 명시하면 서브 도메인까지 포함되도록 쿠키의 범위가 확장된다.
    예를 들어 Domain 속성을 test.com으로 설정하면 브라우저는 a.test.com으로 부터 받은 쿠키를 b.test.com으로도 보내게 된다. 그러므로 a.test.com과 b.test.com이 쿠키를 공유하는 효과가 발생한다.

    HTTP응답
    Set-Cookie: <쿠키 이름>=<쿠키 >; Domain=도메인

  • Set-Cookie Response Headers에 Path 속성을 명시하면 해당 도메인의 특정 경로로 쿠키의 범위를 축소시킬 수 있다.
    예를 들어 Path 속성이 /users 라고 설정되어 있는 쿠키는 브라우저가 /users를 포함한 하위 경로로 요청할 때만 서버로 돌려 보내진다.

    HTTP응답
    Set-Cookie: <쿠키 이름>=<쿠키 >; Path=경로

쿠키의 한계

  1. 쿠키는 유실되기 쉽다.
    대부분 브라우저는 환경 설정에서 쿠키 일괄 삭제 기능을 제공하며, 웹사이트 별로도 어렵지 않게 쿠키를 삭제할 수 있다. 또한 특정 브라우저가 하드디스크의 어느 경로에 쿠키를 저장하는지는 구글링을 해보면 누구나 쉽게 파악할 수 있어서 통째로 폴더를 삭제해버릴 수도 있다. 따라서 유실되면 안되는 중요한 데이터는 쿠키를 사용해서 브라우저에 저장하면 안된다.

  2. 쿠키는 변조되기 쉽다.
    브라우저의 개발자 도구를 사용하면 각 웹사이트 별로 현재 어떤 쿠키가 저장되어 있는지 한 눈에 파악할 수 있으며 쿠키를 손쉽게 변경할 수 있다. 이를 통해 서버가 브라우저로 보낸 쿠키와 전혀 다른 쿠키를 서버에 돌려 보낼 수 있으며, 심지어 새로운 쿠키를 만들어서 서버에 보낼 수도 있다. 뿐만 아니라 프록시 서버를 이용해서 브라우저에서 보낸 쿠키를 중간에 변조하여 서버에 보내는 것도 가능하다. 따라서 서버에서는 브라우저로부터 수신한 쿠키 데이터가 유효한지 검증할 필요가 있다.

  3. 쿠키는 도난되기 쉽다.
    클라이언트의 컴퓨터에 저장되는 쿠키는 항상 온갖 해킹의 위험에 노출되어 있다. 아무리 클라이언트 단에서 보안에 신경쓴다해도 기업이 소유하고 있는 서버 단의 보안 수준에 비할 바가 못된다. 따라서 개인 정보와 같이 민감한 데이터를 쿠키를 사용해서 저장하면 곤란한 일이 벌어질 수도 있다.

  4. 네트워크 대역폭 낭비
    HTTP 요청 시 헤더에 쿠키가 같이 나가기 때문에 쿠키 사이즈가 커지면 HTTP 요청 크기도 커진다. (+ 쿠키는 사이즈에 제한이 있다.)
    또한 매 요청마다 같은 데이터가 서버로 전송됨에 따라 네트워크 대역폭이 낭비된다. 따라서 서버 단에서 읽을 필요없이 순수하게 브라우저에 저장해도 무방한 데이터라면 굳이 쿠키를 통해 네트워크로 주고 받을 필요가 없다.


쿠키의 보안 속성

쿠키의 한계와 대체 기술(웹 스토리지)에도 불구하고 반드시 쿠키를 사용해야하는 상황이라면 가급적 보안 속성을 사용해야 한다.

  1. Secure
    Set-Cookie Response Headers에 이 속성이 명시된 쿠키는 https 프로토콜 상에서만 서버로 돌려 보내진다. 네트워크 상에서 탈취되었을 때 문제가 될 수 있는 쿠키에 쓰면 유용하다.
    HTTP응답
    Set-Cookie: <쿠키 이름>=<쿠키 >; Secure
  2. HttpOnly
    Set-Cookie Response Headers에 이 속성이 명시된 쿠키는 브라우저에서 자바스크립트로 Document.cookie 객체를 통해 접근할 수 없다.
    HTTP응답
    Set-Cookie: <쿠키 이름>=<쿠키 >; HttpOnly

Document.cookie

자바스크립트의 Document.cookie 객체를 통해 쿠키를 create, delete, read 할 수 있다. document.cookie는 name=value 쌍으로 구성되어 있고, 각 쌍은 ; 으로 구분한다. 쌍 하나는 하나의 독립된 쿠키를 나타낸다.

// 쿠키 read 
const cookies = document.cookie;
cookies.split(';')


// 쿠키 creat
document.cookie = "color=red"; // 이전 쿠키를 덮어쓰지 않고 새 쿠키 추가

document.cookie = "color1=blue";
document.cookie = "color2=yellow";
document.cookie = "color3=white";

document.cookie = "color=black"; // red → black으로 값 변경


// 쿠키 delete : expire or max-age 속성 이용 
// 1시간 뒤에 쿠키가 삭제됩니다.
document.cookie = "max-age=3600";
 
// 만료 기간을 0으로 지정하여 쿠키를 바로 삭제함
document.cookie = "max-age=0";

쿠키의 유효기간

Expires은 쿠키의 종료시점을 지정하는 속성이다. GMT or UTC 형식으로 날짜를 입력해야 하므로 new Date().toGMTString()을 이용한다. Expires 속성을 입력하지 않으면 브라우저가 종료될 때 쿠키가 삭제된다.

max-age는 유효기간을 지정하는 속성으로 단위는 1초이다. 현재부터 설정하고자 하는 만료 일시까지의 시간을 초로 환산한 값을 설정한다.

// 하루 뒤 만료되는 쿠키 설정하기
const date = new Date();
date.setDate(date.getDate() +1);

document.cookie = 'user=NHS; expires=`${date.toGMTString()}`'; 


// max-age 이용하기
document.cookie = 'user=NHS; max-age=60'; // 1분 뒤 (단위는 1초) 

Expires, Max-Age 속성이 둘 다 있을 경우 Max-Age 속성이 우선시 된다. 두 가지 속성이 모두 없을 때 해당 쿠키는 세션이 종료될 때 만료되는 세션 쿠키가 된다.




Web Storage

웹 스토리지는 클라이언트에서 데이터를 저장하기 위한 새로운 방법으로 HTML5부터 등장했다. 웹 스토리지에는 Local Storage, Session Storage가 있다. 세션 스토리지는 웹페이지의 세션이 끝날 때 저장된 데이터가 지워지는 반면, 로컬 스토리지는 웹페이지의 세션이 끝나더라도 데이터가 지워지지 않는다.

브라우저에서 같은 웹사이트를 여러 탭이나 창에 띄우면, 여러 개의 세션 스토리지에 데이터가 서로 격리되어 저장되며, 각 탭이나 창이 닫힐 때 저장된 데이터가 함께 사라진다.
반면, 로컬 스토리지의 경우 여러 탭이나 창 간에 데이터가 서로 공유되며 탭이나 창을 닫아도 데이터는 브라우저에 그대로 남아있다.

이러한 로컬 스토리지의 데이터 영속성은 어디까지나 동일한 컴퓨터에서 동일한 브라우저를 사용할 때만 해당한다. 즉, 같은 컴퓨터에서 다른 브라우저를 사용하거나 (e.g. 크롬 → 사파리), 또는 다른 컴퓨터에서 같은 브라우저를 사용하는 경우에는 (e.g. 집에서 크롬을 쓰다가 회사에서 크롬을 쓰면) 엄연히 다른 브라우저이므로 서로 다른 두 개의 로컬 스토리지에 데이터가 저장된다.

🌨️ 참고로 다른 기기나 브라우저 간에 데이터가 공유되고 영속되야 한다면 클라우드 플랫폼이나 DB 서버를 사용해야한다.
웹 스토리지로 클라우드 서비스 or DB 서버를 대체할 수는 없지만, 프로토타입을 만들거나 데이터가 중요하지 않은 개인 프로젝트에서는 유용하게 사용할 수 있다.


기본 API

웹 스토리지는 기본적으로 keyvalue로 이루어진 데이터를 저장할 수 있다. 자바스크립트 API의 기본적인 사용 방법은 다음과 같다.

// 키에 데이터 쓰기
localStorage.setItem("key", value);

// 키로부터 데이터 읽기
localStorage.getItem("key");

// 키의 데이터 삭제
localStorage.removeItem("key");

// 모든 키의 데이터 삭제
localStorage.clear();

// 저장된 키-값 쌍의 개수
localStorage.length;

웹 스토리지를 사용할 때 주의해야 할 부분은 string 데이터 타입만 지원한다는 것이다. 따라서 숫자형 데이터를 로컬 스토리지에 쓰고 다시 읽어보면 숫자가 아닌 문자가 나온다.

localStorage.setItem('num', 1)
localStorage.getItem('num') === 1 // false
localStorage.getItem('num') // "1"
typeof localStorage.getItem('num') // "string"

이와 같은 문제를 피하기 위해서 많이 사용하는 방법이 JSON 형태로 데이터를 읽고 쓰는 것이다.

  • JSON.stringfy 로컬 스토리지에 쓸 데이터를 JSON 형태로 직렬화하고,
  • JSON.parse 읽은 데이터를 JSON 형태로 역직렬화해주면 원본의 데이터를 그대로 얻을 수 있다.
    const user_1 = {
       name: 'NHS',
       age: 25, // 숫자형 
       tech: ['MAC', 'AIRPOD']
    }
    localStorage.setItem('user', JSON.stringify(user_1)) // 데이터 쓰기 
    const storedName = JSON.parse(localStorage.getItem('user'))  // 불러오기 


데이터 청소

로컬 스토리지에 저장된 데이터는 웹페이지를 닫는다고 해서 사라지지 않으므로 불필요한 데이터가 남지 않도록 직접 청소해주는 것이 좋다.

localStorage.length // 5

localStorage.removeItem('obj')

localStorage.length // 4

localStorage.clear()

localStorage.length // 0

Todo App에 Local Storage 붙이기

방법1

App.js에서 데이터를 저장할 때 로컬 스토리지에도 저장하도록 코드를 수정하자.

function App({$target, initialState}) {
    new Header({
        $target, 
        text: 'simple Todo List'
    })
    
    new TodoForm({
        $target,
        onSubmit: (text) => {
            const nextState = [...todoList.state, { text }]
            todoList.setState(nextState)
            localStorage.setItem('todos',JSON.stringify(nextState))
            // 로컬 스토리지에도 저장
        }
    })

    const todoList = new TodoList ({
        $target,
        initialState
    })
}

main.js의 초기 데이터를 삭제하고, 대신 로컬 저장소에서 데이터를 꺼내오도록 하자.

// 소토리지 값이 없으면 빈 배열 반환
const initialState = Json.parse(localstorage.getItem('todos') || '[]')

const $app = document.querySelector('.app')

new App({
    $target: $app,
    initialState
})

❗이러한 방법은 조작에 의해 웹페이지가 작동하지 않을 수 있다. 만약 외부 툴 등을 이용해 todos json string을 올바르지 않은 형태로 바꾸면 어떻게 될까?
예를 들어 직접 개발자 도구에 들어가 Local Storage의 value 값을 바꿔버리면 웹 페이지가 작동이 안된다. 이런 문제에 대비해 storage.js 파일을 만들고 여기에서만 Local Storage에 접근하도록 만든다.

Local Storage, Session Storage 모두 용량 제한이 있는데, 캐싱을 잘못 설계하면 예를 들어 캐시를 날리는 코드를 안짜면 캐시가 너무 많이 쌓여서 setItem 사용 시 오류가 날 수 있다.


방법2

storage.js

  • setItem() 함수에서는 key를 바탕으로 로컬 스토리지에 value를 저장하고, 에러 발생 시 콘솔에 찍어준다.

  • getItem() 함수에서는 key와 defaultValue를 인수로 받아, key를 통해 로컬 스토리지에서 value를 가져올 수 있으면 상태를 업데이트, 아니라면 기본 값 ( [ ] )을 상태로 지정하여 화면에 불러올 수 있도록 한다.

  • 예외 처리(try, catch)는 값을 조작당해도 웹 페이지가 작동하게 한다.

// 전역 오염을 최소화하기 위해 즉시 실행 함수로 한 번 감싸준다. 
// 인수로는 window.localStorage를 넘긴다. 

const storage = (function(storage){
    const setItem = (key, value) => {
        try {
            storage.setItem(key,value)
        } catch(e) {
            console.log(e)  
        }
    }
    const getItem = (key, defaultValue) => {
        try {
            const storedValue = storage.getItem(key)

            if(storedValue) {  // storedValue가 유효하다면 JSON.parse
                return JSON.parse(storedValue)
            }
            return defaultValue  // JSON.parse이 안되는 경우 기본값 
        } catch(e) {
            console.log(e)
            return defaultValue
        }
    }

    return {
        setItem,
        getItem
    }
})(window.localStorage)

App.js

function App({$target, initialState}) {
    new Header({
        $target, 
        text: 'simple Todo List'
    })
    
    new TodoForm({
        $target,
        onSubmit: (text) => {
            const nextState = [...todoList.state, { text }]
            todoList.setState(nextState)
            Storage.setItem('todos',JSON.stringify(nextState))
            // localStorage → Storage로 바꿔줌 
        }
    })

    const todoList = new TodoList ({
        $target,
        initialState
    })
}

main.js

const initialState = Json.parse(storage.getItem('todos',[]))

// localstorage.getItem('todos') || '[]' → storage.getItem('todos',[])

const $app = document.querySelector('.app')

new App({
    $target: $app,
    initialState
})

🐶 테스트



profile
오늘도 신나개 🐶

0개의 댓글