[모던JS: 심화] 브라우저에 데이터 저장하기 (1)

KG·2021년 7월 2일
3

모던JS

목록 보기
43/47
post-thumbnail

Intro

본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.

쿠키와 document.cookie

쿠키는 브라우저에 저장되는 작은 크기의 문자열로 HTTP 프로토콜의 일부이다. 쿠키는 주로 웹 서버에 의해 만들어지는데, 서버가 HTTP 응답 헤더의 Set-Cookie에 내용을 넣어 전달하면 브라우저는 이 내용을 자체적으로 브라우저에 저장한다. 이 저장된 데이터를 쿠키라고 부른다. 개발자도구를 통해 Application 탭에 가보면 왼쪽 메뉴바에서 Cookies라고 써진 메뉴를 볼 수 있다. 브라우저는 사용자가 쿠키를 생성하도록 한 동일 서버(사이트)에 접속할 때마다 쿠키의 내용을 Cookie 요청 헤더에 넣어 항상 함께 전달한다.

쿠키는 클라이언트 식별과 같은 인증에 가장 많이 사용된다.

  1. 사용자가 로그인하면 서버는 HTTP 응답 헤더의 Set-Cookie에 담긴 세션 식별자(Session Identifier) 정보를 사용해 쿠키를 설정

  2. 사용자가 동일 도메인에 접속하려고 하면 브라우저는 HTTP Cookie 헤더에 인증 정보가 담긴 고유값(세션 식별자)을 함께 실어 서버에 요청 전송

  3. 서버는 브라우저가 보낸 요청 헤더의 세션 식별자를 읽고 사용자를 식별

이처럼 쿠키라는 별도의 장치를 함께 전송하여 사용자를 식별하는 이유는 기본적으로 HTTP 통신이 stateless하기 때문이다. HTTP 통신은 요청에 대해 응답을 반환하는 사이클을 돌고나면 이를 위한 연결을 해제하기 때문에, 이전에 있었던 요청들에 대해 기억할 수 없다. 때문에 쿠키를 매 요청마다 실어 보내어 특정 사용자가 이전에도 서버에 요청을 보낸 이력이 있었음을 알려줄 수 있다.

document.cookie 프로퍼티를 이용하면 브라우저에서도 쿠키에 접근할 수 있다. 쿠키와 다양한 쿠키 옵션을 다루는 법을 알아보자.

1) 쿠키 읽기

지금 보고 있는 사이트에서는 어떤 쿠키를 저장하고 있는지 알아보기 위해서는 다음의 코드를 실행하면 된다.

alert( document.cookie );
// cookie1=value1; cookie2=value2;...

document.cookiename=value 쌍으로 구성되어 있고, 각 쌍은 ;로 구분한다. 이때 하나의 쌍은 독립된 쿠키 하나를 나타낸다.

;를 기준으로 document.cookie의 값을 분리하면 원하는 쿠키에 접근할 수 있다. 주로 정규 표현식이나 배열 관련 함수를 사용해서 쿠키를 분리하곤 한다.

몇몇 브라우저는 documnet.cookie를 호출해도 정상적으로 쿠키를 불러올 수 없을 수도 있는데, 이와 관련해서는 밑에서 자세히 살펴보도록 하자.

2) 쿠키 쓰기

document.cookie에 값을 직접 쓸 수 있다. 이때 cookie는 데이터 프로퍼티가 아닌 접근자(getter/setter) 프로퍼티이다. 따라서 접근자 프로퍼티에 값을 할당하는 것처럼 값을 할당할 수 있다. 때문에 document.cookie에 값을 할당하면, 브라우저는 이 값을 받아 해당 쿠키만을 갱신한다. 즉 다른 쿠키의 값이나 쿠키 전체 값을 덮어씌우지 않는다.

document.cookie = "user=John";	// 이름이 'user'인 쿠키 값만 갱신
alert( document.cookie );	// 모든 쿠키 보여주기

쿠키의 이름과 값에는 특별한 제약이 없어 모든 글자가 이론적으로는 허용된다. 하지만 보통 형식의 유효성을 일관성 있게 유지하기 위해 내장 함수 encodeURIComponent를 사용해 이름과 값을 이스케이프 처리해서 저장하는 것을 권고한다.

// 공백문자가 포함됨
let name = "my name";
let value = "John Smith";

// 공백문자는 특수 값이므로 이스케이프 처리 (인코딩)
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);

alert( docuemnt.cookie );	// ...; my%20name=John%20Smith;

이때 쿠키는 기본적으로 가지고 있는 한계가 있다.

  • encodeURIComponent로 인코딩 한 이후의 name=value 쌍은 그 크기가 4KB를 넘을 수 없다. 이를 넘어가는 경우 쿠키에 저장이 불가하다.

  • 도메인 하나 당 저장할 수 있는 쿠키의 개수는 20여개로 한정되어 있다. 이때 개수는 브라우저별로 조금씩 다를 수 있다.

3) 쿠키 옵션

쿠키에는 지정할 수 있는 몇 가지 옵션이 있다. 이때 몇몇 옵션은 아주 중요한 역할을 하기 때문에 꼭 지정해 줄 필요가 있다. 옵션은 key=value 뒤에 나열하고 마찬가지로 ;로 구분한다.

document.cookie = "user=John; path=/; expires=Tue, 19 Jan 2038 04:14:05 GMT";

쿠키에 지정할 수 있는 주요 옵션에 대해 알아보자.

1. path

  • path=/mypath

URL path(경로)의 접두사로, 해당 경로나 해당 경로 하위 경로에 있는 페이지만 쿠키에 접근할 수 있음을 명시한다. 절대경로로 지정되어야 하고, 미 지정시에 기본값은 현재 경로이다.

예를 들어 path=/admin 옵션을 사용해 설정한 쿠키는 /admin/admin/something 경로에서는 볼 수 있지만 /home 이나 /adminpage 등의 경로에서는 열람할 수 없다.

특별한 경우가 아니라면 보통 path 옵션을 path=/ 같이 루트로 설정해 웹사이트 모든 페이지에서 쿠키에 접근할 수 있도록 허용한다.

2. domain

  • domain=site.com

쿠키에 접근 가능한 도메인을 지정한다. 다만 몇 가지 제약이 있어서 아무 도메인이나 지정할 수는 없다. domain 옵션에 아무 값도 넣지 않았다면, 쿠키를 설정한 도메인에서만 쿠키에 접근할 수 있다. 예를 들어 site.com에서 설정한 쿠키는 other.com에서 접근할 수 없다.

이 외에 까다로운 제약사항이 또 있는데, 서브 도메인인 forum.site.com 역시 쿠키 정보에 접근할 수 없다는 점이다.

// site.com에서 쿠키를 설정
document.cookie = "user=John";

// forum.site.com에서는 user 쿠키 접근이 불가
alert( document.cookie );

서브도메인을 포함한 다른 도메인에서 쿠키에 접속할 방법은 없다. 이러한 제약사항은 보안성과 안정성을 높이기 위해 고안되었다. 보통 쿠키에는 사용자 인증과 관련된 민감한 정보가 저장되는 경우가 많기 때문에, 저장된 쿠키를 오직 관련된 페이지에서만 열람 가능하도록 설정한다.

그렇지만 서브도메인에서도 쿠키를 받아볼 수 있는 방법이 있다. site.com에서 쿠키를 설정할 때 domain 옵션에 루트 도메인인 domain=site.com을 명시적으로 설정해주면, 서브도메인에서도 정상적으로 쿠키를 받아볼 수 있다.

// site.com에서 쿠키를 설정
// domain 옵션에 루트 도메인 지정
document.cookie = "user=John; domain=site.com";

// forum.site.com에서도 user 쿠키 접근 가능
alert( document.cookie );

하위 호환성을 유지하기 위해 domain=.site.com도 이와 동일하게 작동한다. 만약 구식 브라우저를 지원하려면 해당 표기법을 이용해야 할 수 있다.

이와 같이 domain 옵션값을 적절히 사용하면 서브 도메인에서도 쿠키에 접근이 가능하다.

3. expires와 max-age

expires(유효기간)max-age(만료기간) 옵션은 모두 쿠키의 생존시간을 설정하는 옵션이다. 해당 옵션이 지정되어 있지 않으면 브라우저가 닫힐 때 쿠키도 자동으로 함께 삭제된다. 이런 쿠키를 보통 세션 쿠키(session cookie)라고 부른다.

expiresmax-age 옵션을 설정하면 브라우저를 닫아도 지정된 시간만큼 쿠키는 계속 살아있게 된다.

  • expires=Tue, 19 Jan 2038 03:14:07 GMT

브라우저는 설정된 유효 일자까지 쿠키를 유지하다가, 해당 일자에 도달하면 쿠키를 자동으로 삭제한다. 이때 쿠키의 유효 일자는 반드시 GMT(Greenwich Mean Time) 포맷으로 설정되어야 한다. 이는 date.toUTCString을 사용해서 쉽게 변경할 수 있다. 아래는 유효 기간이 하루인 쿠키를 만드는 예시이다.

// 지금으로부터 하루 후
let date = new Date(Date.now() + 86400e3);
date = date.toUTCString();
document.cookie = "user=John; expires=" + date;

만약 expires 옵션을 과거 시간대로 지정하면 쿠키는 바로 삭제된다.

  • max-age=3600

max-ageexpires 옵션의 대안으로, 쿠키 만료 시간을 설정할 수 있다. 현재부터 설정하고자 하는 만료일시까지 시간을 초로 환산한 값을 설정한다. 만약 0이나 음수값을 설정하는 경우 쿠키는 바로 삭제된다.

// 1시간 뒤 쿠키는 삭제
document.cookie = "user=John; max-age=3600";

// 쿠키 바로 삭제
documnet.cookie = "user=John; max-age=0";

4. secure

  • secure

이 옵션을 설정하면 HTTPS 프로토콜로 통신하는 경우에만 쿠키가 전송된다. secure 옵션이 없으면 기본 설정이 적용되어 HTTP/HTTPS 모두 쿠키를 읽을 수 있다. 쿠키는 기본적으로 도메인만 확인하고 프로토콜을 따지지 않기 때문이다.

하지만 secure 옵션이 설정된 경우 https://...에서 설정된 쿠키는 절대 http://... 도메인에서 접근할 수 없다. 쿠키에 민감한 내용이 저장되어 있어 암호화 되지 않은 HTTP 연결을 통해 전달되는 것을 원치 않는다면 해당 옵션을 사용하면 된다.

// https 프로토콜로 통신하고 있다고 가정
// 설정된 쿠키는 HTTPS 통신 시에만 접근 가능
document.cookie = 'user=John; secure';

5. samesite

또 다른 보안 속성인 samesite 옵션은 크로스 사이트 요청 위조(Cross-Site Request Forgery, XSRF) 공격을 막기 위해 만들어진 옵션이다. 아래 XSRF 공격 시나리오를 통해 이 속성의 동작 방식과 언제 이 속성을 유용하게 사용할 수 있는지 알아보자.

XSRF(CSRF) 공격

현재 bank.com에 로그인되어 있다고 가정해보자. 해당 사이트에서 사용되는 인증 쿠키가 브라우저에 저장되고, 브라우저는 bank.com에 요청을 보낼 때마다 인증 쿠키를 함께 전송할 것이다. 서버는 전송받은 쿠키를 이용해 사용자를 식별하고, 보안이 필요한 재정 거래를 처리하게 될 것이다.

이제 로그아웃을 하지 않고 다른 창을 띄워 웹 서핑을 하던 도중, 뜻하지 않게 evil.com에 접속했다고 가정해보자. 해당 사이트는 악의를 가진 해커가 송금을 요청하는 폼 <form action="https://bank.com/pay">을 만들어두고 진입 시 자동으로 제출되게 코드가 짜여있다.

폼이 evil.com에서 bank.com/pay로 바로 전송될 때 인증 쿠키도 함께 전송된다. 현재 브라우저에 bank.com에서 설정한 쿠키 정보가 저장되어 있고, bank.com에 요청을 보낼 때마다 해당 사이트에서 설정한 쿠키가 전송되기 때문이다. 은행은 전송받은 쿠키를 읽어 이를 정상적인 요청이라 간주하고 돈을 송금하게 된다.

이러한 공격을 크로스 사이트 요청 위조, XSRF 공격이라고 부른다.

물론 실제 은행은 당연히 이러한 공격을 막을 수 있도록 시스템을 설계한다. bank.com에서 사용하는 모든 폼에 XSRF 보호 토큰이라는 특수 필드를 삽입하는데, 이 토큰은 악의적인 페이지에서 만들 수 없고 원격 페이지에서도 훔쳐올 수 없도록 구현되어 있다. 따라서 외부로부터 악의를 가진 요청이 오더라도 보호 토큰이 없거나, 서버에 저장된 값과 일치하지 않으면 요청이 무용지물이 된다.

하지만 이런 절차는 구현에 시간이 걸린다는 단점이 있다. 존재하는 모든 폼에 보호 토큰을 세팅해야 한다는 것도 여간 번거로운 일이 아니다. 그리고 요청 전부를 일일이 검사하는 작업도 필요하다.

samesite 옵션

쿠키의 samesite 옵션을 이용하면 XSRF 보호 토큰 없이도 이론적으로 크로스 사이트 요청 위조를 막을 수 있다. 이 옵션에는 다음과 같은 두 가지 값을 설정할 수 있다.

  • samesite=strict

이는 값을 설정하지 않고 그냥 samesite만 기입해도 동일하게 동작한다. 사용자가 외부에서 요청을 보낼 때, samesite=strict 옵션이 있는 쿠키는 절대 전송되지 않는다.

메일에 있는 링크를 따라 접속하거나 evil.com과 같은 사이트에서 폼을 전송하는 경우 등 제 3의 도메인에서 요청이 이뤄지는 경우 쿠키가 전송되지 않는다.

인증 쿠키에 samesite 옵션이 있다면 XSRF 공격은 절대 성공할 수 없다. evil.com 에서 전송하는 요청에는 쿠키 정보가 함께 전송되지 않을 것이고, 따라서 bank.com은 식별할 수 없는 사용자로 판단해 결재 처리를 허용하지 않을 것이다. 이 보호장치는 꽤 믿음직하게 작동한다.

그러나 이 옵션은 약간의 불편함을 수반할 수 있다.

만약 사용자가 메모장 등에 bank.com에 요청을 보낼 수 있는 링크를 따로 보관하고 있다가, 링크를 클릭해 접속하는 경우에도 이는 외부 접근으로 인식하기 때문에 쿠키 정보가 함께 전송되지 않는다.

보통 이러한 문제는 두 개의 쿠키를 함께 사용해 해결할 수 있다. 예를 들어 환영 메시지를 출력해주는 일반 인증용 쿠키와, 데이터 교환 시 사용하는 samesite=strict 옵션을 지정한 쿠키를 따로 둘 수 있다. 이 경우 외부에서 접근하더라도 사용자는 정상적으로 환영 메시지를 볼 수 있고, 결재 처리는 무조건 은행 사이트를 통해서만 수행되도록 만들 수 있다.

  • samesite=lax

samesite=lax는 사용자 경험을 크게 해치지 않으면서도 XSRF 공격을 막을 수 있는 느슨한 보안 정책이다. strict와 마찬가지로 lax 역시 사이트 외부에서 요청을 보낼 때 브라우저가 쿠키를 전송하는 것을 방지한다. 하지만 다음 두 조건을 동시에 만족하는 경우엔 예외에 해당하며, samesite=lax 옵션을 설정한 쿠키가 전송된다.

  1. 안전한 HTTP 메서드인 경우 (eg. GET)

    안전한 메서드는 읽기 작업만 수행하고 쓰기나 데이터 교환 작업을 수행하지 않는 메서드를 의미한다. 링크를 따라가는 행위는 항상 GET 방식이기 때문에 안전한 메서드만 쓰인다.

  2. 작업이 최상위 레벨 탐색에서 이루어질 때

    브라우저 주소창에서 URL을 변경하는 경우처럼 대다수의 작업은 이 조건을 충족한다. 하지만 <iframe> 안에서 탐색이 일어나는 경우는 최상위 레벨 탐색이 아니기 때문에 이 조건을 충족하지 못한다. 또한 AJAX 요청 또한 탐색 행위가 아니므로 이 조건을 충족할 수 없다.

브라우저를 이용해 자주 하는 작업인 특정 URL로 이동하기는 samesite=lax 옵션이 설정되어 있으면 쿠키가 서버로 전송된다. 이는 위 두 가지 조건을 모두 만족하기 때문이다. 따라서 메모장과 같은 문서에 링크를 기입 후 클릭을 통해 이동하더라도 정상적으로 쿠키가 전송된다.

하지만 외부 사이트에서 AJAX 요청을 보내거나 폼을 전송하는 등의 복잡한 작업을 시도할 땐 쿠키가 전송되지 않는다. 이런 제약사항이 있어도 괜찮다면 samesite=lax는 사용자 경험을 크게 해치지 않으면서 동시에 보안성도 챙길 수 있는 방법으로 활용할 수 있다.

samesite는 훌륭한 보안을 제공하는 옵션이지만 오래된 브라우저에서는 아직 이 옵션을 지원하지 않는 경우가 종종있다. 따라서 samesite 옵션으로만 보안 처리를 하는 경우 구식 브라우저에서는 여전히 보안에 대한 위협이 존재할 수 있다. 때문에 이 외에도 별도의 보안 규칙을 마련하는 것이 더 엄격한 보안을 위해 추천되는 편이다.

6. httpOnly

이 옵션은 자바스크립트와는 전혀 관련이 없지만 쿠키 옵션 중에 하나이므로 잠깐 살펴보도록 하자.

httpOnly 옵션은 웹서버에서 Set-Cookie 헤더를 이용해 쿠키를 설정할 때 지정할 수 있다. 이 옵션이 지정되면 자바스크립트 같은 클라이언트 측 스크립트가 쿠키를 사용할 수 없다. 즉 document.cookie를 통해 쿠키를 읽거나 조작하는 행위가 금지된다.

쿠키에는 사용자 인증 정보가 들어있는 경우가 많기 때문에, 악의를 가진 해커가 자바스크립트를 이용해 쿠키를 탈취하거나 쿠키를 이용한 공격을 예방하기 위해서 이 옵션을 사용할 수 있다.

4) 쿠키 함수

쿠키를 다룰 때 유용하게 사용할 수 있는 몇 가지 함수가 있다. 이는 document.cookie를 수동으로 조작하지 않고도 좀 더 편리하게 쿠키를 다룰 수 있는 기능을 제공한다.

유사한 기능을 하는 다양한 쿠키 라이브러리가 많이 있기 때문에, 해당 코드는 일종의 데모 목적으로 살펴보고 넘어가자. 데모이지만 실제 환경에서도 정상 작동하는 함수이긴 하다.

1. getCookie(name)

정규 표현식을 사용해 쿠키에 접근하도록 하는 함수이다. name에 해당하는 쿠키를 반환하도록 내부에서 정규표현식을 이용해 값을 추출한다.

// 주어진 이름의 쿠키를 반환
// 만약 조건에 맞는 쿠키가 없다면 undefined 반환
function getCookie(name) {
  let matches = documnet.cookie.match(new RegExp(
    "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
  ));
  return matches ? decodeURIComponent(matches[1]) : undefined;
}

주의할 점은 쿠키값은 인코딩되어 있는 상태이기 때문에 getCookie는 내장 함수인 decodeURIComponent를 사용해 디코딩한 값을 반환하고 있다는 것이다.

2. setCookie(name, value, options)

현재 경로(path=/)를 기본으로 주어진 namevalue를 가진 쿠키를 설정한다. options으로 다른 쿠키 옵션을 설정할 수 있다.

function setCookie(name, value, options) {
  options = {
    path: '/',
    ...options
  };
  
  if (options.expires instanceof Date) {
    options.expires = options.expires.toUTCString();
  }
  
  let updatedCookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);
  
  for (let optionKey in options) {
    updatedCooke += "; " + optionKey;
    let optionValue = options[optionKey];
    
    if (optionValue !== true) {
      updatedCookie += "=" + optionValue;
    }
  }
  
  document.cookie = updatedCookie;
}

setCookie('user', 'John', { secure: true, 'max-age' : 3600 });

3. deleteCookie(name)

만료 기간을 음수로 설정하면 쿠키를 삭제할 수 있다.

function deleteCookie(name) {
  setCookie(name, "", {
    'max-age': -1,
  });
}

이때 갱신이나 삭제는 동일한 도메인과 경로에서만 수행되어야 한다.

5) 서드 파티 쿠키

웹 서핑을 하다보면 자신도 모르게 여러 사이트로부터 쿠키 정보가 많이 쌓여있는 것을 확인할 수 있다. 이들은 과거에 직접 방문을 통해 받은 쿠키 정보일 수도 있지만, 방문한 적이 없는데도 저장된 쿠키일 수 있다. 이처럼 사용자가 방문 중인 도메인이 아닌 다른 도메인에서 설정한 쿠키를 서드 파티 쿠키라고 부른다.

  1. site.com의 특정 페이지에서 이미지 배너를 불러온다고 가정해보자. 배너는 다른 도메인 <img src="https://ads.com/banner.png">에서 가져온다.

  2. ads.com에 있는 원격 서버는 배너와 함께 Set-Cookie 헤더를 전송해 브라우저가 id=1234와 같은 쿠키를 설정하도록 한다. 이 쿠키는 ads.com 도메인에서 설정한 것이기 때문에 ads.com 에서만 열람이 가능하다.

  1. 사용자가 다시 ads.com에 접속하면 원격 서버는 요청과 함께 전송받은 쿠키의 id를 이용해 해당 유저를 인식한다.

  1. 사용자가 site.com을 떠나 other.com에 접속하고 이 사이트에도 배너가 있으면 ads.com은 또 쿠키를 전송받는다. 이 쿠키는 ads.com에서 설정한 것이기 때문이다. 이를 이용해 ads.com은 사용자를 인식하고, 이 사용자가 어떤 사이트로 이동했는지 등을 추적할 수 있다.

광고회사는 사용자의 이용 행태를 추적하고, 광고를 제공하기 위해 오래 전부터 이러한 서드 파티 쿠키를 통해 사용자를 추적하고 있다. 서드 파티 쿠키는 쿠키를 설정한 도메인에 종속되기 때문에 ads.com은 사용자가 어떤 사이트를 방문했는지 추적할 수 있다.

그러나 사람들은 이러한 추적을 일종의 감시로 받아들일 수 있고, 감시받는 것을 좋아하지 않을 수 있다. 브라우저엔 이런 쿠키를 비활성화 할 수 있는 기능이 있는데, 이 기능을 사용하면 추적을 막을 수 있다.

또한 몇몇 모던 브라우저는 아예 서드 파티 쿠키를 위한 특별한 정책을 도입하여 광고회사의 무분별한 추적을 막고 있다.

  • 사파리는 서드 파티 쿠키를 전면적으로 허용하지 않는다.
  • 파이어폭스는 서드 파티 도메인 블랙 리스트를 만들어, 리스트에 오른 도메인의 서드 파티 쿠키는 원천 차단한다.

스크립트 태그로 외부 도메인에서 어떤 스크립트를 읽어오고, 해당 스크립트 안에 document.cookie로 쿠키를 설정하는 코드가 있을 때, 이때 생성되는 쿠키는 서드 파티 쿠키가 아니다.
스크립트는 외부 도메인에서 스크립트를 가져와 실행하는 역할을 한다. 즉 현재 도메인에서 쿠키가 생성되는 것이 되기 때문에, 쿠키는 현재 도메인에 속하게 된다.

6) GDPR

해당 주제는 자바스크립트와는 전혀 상관 없지만 쿠키를 설정할 때 주의해야 할 내용이다. 유럽연합에는 사용자 개인 정보 보호 취급이 매우 세심하고 엄격하기 때문에, 쿠키를 통해 사용자 정보를 다룰 때도 관련 법령이 제정되어 있다. 쿠키를 추적하는 경우 사용자로부터 명시적인 허가를 얻어야 한다는 것이 이 법령의 중요 요건 중 하나이다.

한국의 경우에도 쿠키를 수집하는 경우 안내창을 명시적으로 띄우는 경우가 많긴 하지만, 유럽의 경우에는 관련 사항이 아예 제도적으로 마련되어 있다고 보면 된다. 때문에 만약 유럽에서 호스팅 되는 페이지를 방문해본 적이 있다면, 해당 페이지가 쿠키를 수집하는 경우 작은 창으로 관련 정보를 알려주고 본문의 내용은 옅게 블라인드 처리하는 방식을 취하는 것을 접해보았을 것이다.

이 요건은 쿠키를 이용한 사용자 추적, 식별에 관한 내용을 담고 있다. 따라서 쿠키를 설정하고, 이 쿠키를 그저 정보 저장의 용도로만 사용한다면 법령에서 강제하는 사항을 지킬 필요는 없다. 하지만 인증 세션과 함께 쿠키를 설정하거나 아이디를 추적한다면 반드시 사용자 동의를 얻어야 한다. 위에서 잠깐 설명한것과 같이 대부분의 웹 사이트는 다음 방식으로 GDPR에 대응한다.

  1. 인증된 사용자에 대해서만 추적 쿠키를 설정하려는 경우

    가입 양식에 개인 정보 취급 방침 동의 같은 확인란을 만들고, 사용자가 이에 동의할 경우에만 추적 쿠키 설정

  2. 모든 사용자를 대상으로 추적 쿠키를 설정하려는 경우

    최초 방문자에게 쿠키설정에 대한 동의를 요구하는 작은 창을 보여주고, 사용자가 이에 동의한 경우에만 콘텐츠를 표시하고 추적 쿠키 설정. 새로운 방문자는 이런 절차가 번거롭다가 생각할 수 있다. 콘텐츠를 가리면서 무조건 클릭해야 하는 창은 유저 경험에 매우 안 좋은 영향을 끼치지만, GDPR 준수를 위해 이런 매커니즘이 반드시 필요할 때가 많다.

개인 정보 보호와 관련된 사항이 전 세계적으로 점점 더 중요하고 민감하게 다루어지고 있기 때문에, 한국 역시 관련 사항이 아직 제도적으로 마련되어 있지 않다고 하더라도 UX를 해치치 않는 선에서 이러한 조치를 취하는 것이 좋을 수 있다.

localStorage와 sessionStorage

웹 브라우저는 과거에 단순히 정적 HTML 문서를 렌더링하는 도구에서 어플리케이션(Application)의 레벨까지 발전했다. 그 과정에서 다양한 기능을 지원하기 위해 여러 스펙이 추가되었는데, 웹 스토리지 객체를 이용하면 브라우저 자체적으로 데이터를 저장하고 관리할 수 있다.

웹 스토리지 객체(Web Storage Object)인 localStroagesessionStorage는 브라우저 내에 키-값 쌍을 저장할 수 있게 해준다. 이 둘을 사용하면 페이지를 새로 고침하거나, 심지어 브라우저를 종료했다가 다시 실행하더라도 데이터가 사라지지 않고 보존된다.

그런데 쿠키를 사용해서도 브라우저에 데이터를 저장할 수 있는데 왜 또다른 웹 스토리지 객체를 사용해 데이터를 저장해야 하는지 의문이 들 수 있다. 이에 대한 이유는 다음과 같다.

  • 쿠키와 다르게 웹 스토리지 객체는 네트워크 요청 시 서버로 전송되지 않는다. 이런 특징 때문에 쿠키보다 더 많은 자료를 보관하기 용이하다. 대부분 브라우저가 최소 2MB에서 그 이상의 크기의 웹 스토리지 객체를 저장할 수 있도록 용량을 제공한다. 또한 개발자는 브라우저 내 웹 스토리지 구성 방식을 설정할 수 있다.

  • 쿠키와 또 다른 점은 서버가 HTTP 헤더를 통해 스토리지 객체를 조작할 수 없다는 것이다. 웹 스토리지 객체 조작은 모두 자바스크립트 내에서만 수행된다.

  • 웹 스토리지 객체는 도메인/프로토콜/포트로 정의되는 오리진에 묶여있다. 따라서 프로토콜과 서브 도메인이 다르다면 데이터에 접근할 수 없다.

두 스토리지 객체는 동일한 메서드와 프로퍼티를 제공한다.

  • setItem(key, value) : 키-값 쌍을 설정
  • getItem(key) : 키에 해당하는 값에 접근
  • removeItem(key) : 키와 해당 값 삭제
  • clear() : 모든 키-값 삭제
  • key(index) : 인덱스에 해당하는 키에 접근
  • length : 저장된 항목의 개수

두 스토리지 객체는 Map과 유사하다. 하지만 인덱스를 사용해 키에 접근할 수 있다는 점 등에서 차이가 존재한다.

1) localStorage

localStorage의 주요 기능은 다음과 같다.

  • 오리진이 같은 경우 데이터는 모든 탭과 창에서 공유된다.
  • 브라우저나 OS가 재시작하더라도 데이터가 보존된다.

오리진(도메인/포트/프로토콜)만 같다면 URL 경로는 달라도 동일한 결과를 볼 수 있다. 이는 localStorage가 동일한 오리진을 가진 모든 창에서 공유되기 때문이다. 따라서 한 창에 데이터를 설정하면 다른 창에서 변동 사항을 볼 수 있다.

// 아래 코드를 실행하고 브라우저 종료
localStorage.setItem('test', 1);
// 동일 도메인에 접속 후 아래 코드 실행
// 데이터가 보존됨을 알 수 있음
alert( localStorage.getItem('test') );	// 1

2) 일반 객체처럼 사용하기

localStorage의 키를 얻거나 설정할 때, 아래처럼 일반 객체와 유사한 방법을 사용할 수 있다.

// 키 설정
localStorage.test = 2;

// 키 얻기
alert( localStorage.test );	// 2

// 키 삭제
delete localStorage.test;

이러한 방법은 아직 하위 호환성을 위해 지원되는 것으로, 일반적인 경우에는 다음과 같은 이유로 잘 사용하지 않는다.

  1. 사용자는 lengthtoString, localStorage의 내장 메서드를 키로 설정할 수 있다. 이렇게 되면 getItem/setItem은 정상적으로 작동해도, 일반 객체처럼 다룰 땐 에러가 발생할 수 있다.
let key = 'length';
localStorage[key] = 5;	// TypeError
  1. 데이터를 수정하는 경우 storage 이벤트가 발생하는데, 객체처럼 취급하는 경우엔 해당 이벤트가 발생하지 않는다. storage 이벤트에 대해선 밑에서 자세하게 다뤄보자.

3) 키 순회하기

localStorage는 키를 사용해 값을 얻고 설정하고, 삭제할 수 있다. 그렇다면 키나 값 전체는 어떻게 얻을 수 있을까? 아쉽게도 스토리지 객체는 이터러블 객체가 아니기 때문에 for...of 반복문을 통해 순회할 수 없다.

따라서 배열처럼 다루어 전체 키-값에 접근해야 한다.

for (let i = 0; i < localStorage.length; i++) {
  let key = localStorage.key(i);
  alert(`${key}: ${localStorage.getItem(key)}`);
}

일반 객체를 다룰 때 처럼 for...in 반복문으로 순회할 수도 있지만, 이 경우에는 필요하지 않은 내장 필드까지 출력된다. 따라서 hasOwnProperty를 이용해 프로토타입에서 상속받은 필드를 골라낼 필요가 있다.

for (let key in localStorage) {
  if (!localStorage.hasOwnProperty(key)) {
    continue;	// setItem, getItem 등의 키는 패스
  }
  alert(`${key}: ${localStorage.getItem(key)}`);
}

아니면 아래처럼 Object.keys()로 자기자신의 키를 먼저 받아온 다음 순회하는 방법도 있다. Object.keys()는 해당 객체에서 정의한 키만 반환하고, 프로토타입에서 상속받은 키는 무시한다.

let keys = Object.keys(localStorage);
for (let key of keys) {
  alert(`${key}: ${localStorage.getItem(key)}`);
}

4) 문자열만 사용

localStorage의 키와 값은 항상 반드시 문자열이어야 한다. 숫자나 객체 등 다른 자료형을 사용하게 되면 문자열로 자동 변환한다.

localStorage.user = { name: 'KG' };
alert( sessionStorage.user );	// [object Object]

이때 JSON 내장메서드를 사용해서 문자열 외의 데이터를 변환하여 보관하고, 받아올 땐 다시 파싱하는 식으로 응용할 수 있다.

localStorage.user = JSON.stringify({ name: 'KG' });

let user = JSON.parse( localStorage.user );
alert( user.name );

디버깅 등의 목적으로 스토리지 객체 전체를 문자열로 변환하는 것도 가능하다.

alert( JSON.stringify(localStorage, null, 2) );

5) sessionStorage

sessionStorage 객체는 localStorage 객체에 비해 자주 사용되지 않는다. 제공하는 프로퍼티와 메서드는 모두 동일하지만, 훨씬 제한적이기 때문이다.

  • sessionStorage는 현재 떠 있는 탭 내에서만 유지된다.
    • 같은 페이지라도 다른 탭에 있으면 다른 곳에 저장
    • 그런데 하나의 탭에 여러 개의 iframe이 있는 경우엔 동일한 오리진으로 취급하기 때문에 sessionStorage는 공유
  • 페이지 새로 고침시 sessionStorage에 저장된 데이터는 보존되지만, 탭을 닫고 새로 열게 되면 사라진다.

이와 같이 sessionStorage는 오리진뿐만 아니라 브라우저 탭에도 종속되어 있기 때문에 잘 사용하지 않는다. 지원되는 메서드와 프로퍼티, 그리고 사용 방법은 모두 위에서 살펴본 localStorage와 동일하다.

6) storage 이벤트

localStoragesessionStorage의 데이터가 갱신될 때 storage 이벤트가 발생한다. 해당 이벤트는 다음과 같은 프로퍼티를 지원한다.

  • key : 변경된 데이터의 키
  • oldValue : 이전 값 (키가 새로 추가되었다면 null)
  • newValue : 새로운 값 (키가 삭제되었다면 null)
  • url : 갱신이 일어난 문서의 URL
  • storageArea : 갱신이 일어난 localStoragesessionStorage 객체

여기서 중요한 점은 storage 이벤트가 이벤트를 발생시킨 스토리지를 제외하고 스토리지에서 접근 가능한 window 객체 전부에서 일어난다는 점이다.

두 개의 창에 같은 사이트를 띄워놨다고 가정해보자. 창은 다르지만 오리진을 공유하므로 두 창의 localStorage는 서로 동일하다. 이때 두 창 모두 storage 이벤트를 수신하기 때문에, 한 창에서 아래 예시를 실행해 데이터를 갱신하면 다른 창에 해당 사항이 반영되어 alert 창이 뜨는 것을 확인할 수 있다.

// 문서는 다르지만, 갱신은 같은 스토리지에 반영
window.onstorage = event => {
  if (event.key != 'now') return;
  alert(evnet.key + ':' + event.newValue + " at " + evnet.url);
};

localStorage.setItem('now', Date.now());

storage 이벤트의 또 다른 중요한 특징은 event.url이 있어 데이터가 갱신된 문서의 URL을 파악할 수 있다는 점이다.

또한 evnet.storageArea에는 스토리지 객체가 포함되어 있는데, storage 이벤트는 localStoragesessionStorage가 변경될 때 모두 발생하기 때문에 event.storageArea는 스토리지 종류에 상관없이 실제 수정이 일어난 것을 참조한다는 것 역시 중요한 특징이다. 변경이 일어났을 때 event.storageArea에 무언가를 설정해 응답이 가능하도록 할 수 있다.

이런 특징을 잘 이용하면 오리진이 같은 창끼리 메시지를 교환하게 할 수도 있다. 모던 브라우저는 오리진이 같은 창끼리 통신할 수 있도록 해주는 브로드캐스트 채널 API(Broadcast Channel API)를 지원한다. 해당 API는기능은 풍부하지만 아직 많은 곳에서 지원하고 있지 않다. 이때 localStorage를 기반으로한 여러 폴리필들이 있는데, 이런 라이브러리들은 브라우저와 관계없이 어디서든 창 간 메시지를 교환할 수 있게 해준다는 장점이 있다.

References

  1. https://ko.javascript.info/data-storage
profile
개발잘하고싶다

0개의 댓글