document.cookie


들어가며

Frontend에서 Cookie를 다룰 때, 일반적으로 javascript의 document.cookie 를 통해 get, set, delete 할 수 있다. 이러한 cookie는 같은 브라우저 내에서 값을 저장, 전달, 공유할 수 있다는 점에서 유용하게 사용된다. 현재 투입되어있는 프로젝트에서도 document.cookie로 쿠키가 관리되고있으며, 다음과 같은 용도로 사용되고있다.

  • 다른 화면으로 파라미터를 넘기기 위해

  • 뒤로가기 시 이전의 검색조건 값을 다시 불러오기 위해

  • window.open 을 통한 팝업 창에서 부모 창으로 값을 전달하기 위해

문제점

이러한 document.cookie를 단순하게 사용하기에는 한 가지 문제가 있었다. 사용자가 목록페이지에서 “각기 다른 여러 글”을 Ctrl을 누른채 클릭하여 새로운 탭으로 띄울때, 그리고 이 과정이 짧은 시간에 이루어질때 몇가지 문제가 발생했다. A, B, C 글을 짧은시간에 ‘거의’ 동시에 띄울경우 A, B, C 글에 대한 각각의 탭이 열리는게 아닌, C의 글만 3개가 띄워진 것. 이는 A탭이 열린 직 후 A에 대한 파라미터를 담은 쿠키를 조회하기 전, C에 대한 쿠키로 덮어쓰기되어 발생된 현상이었다.

그리고 자주 발생하지는 않지만 간혹 A의 탭만 정상적으로 출력되고 나머지는 데이터가 보이지 않는 현상도 있었다. 결과적으로 document.cookie가 비동기적으로 작동한다는 특징 때문에 발생되는 현상이었다.

그래서 document.cookie를 사용할 때는 다음과 같은 상황을 고려해야한다.

  1. document.cookie 객체는 값 변경시 즉시 업데이트 되지 않기 때문에, 짧은시간 내 조회시 이전 값이 반환될 수도 있다.

  2. 쿠키의 “생성 > 조회 > 삭제”가 짧은시간에 여러번 실행되는 경우 정확한 데이터를 보장할 수 없다.

이를 해결하기 위해 localStorage, sessionStorage를 사용한 방식을 생각해볼 수도 있었다. 두개 모두 기본적으로 간단한 데이터를 브라우저에 저장할 수 있도록 지원하지만 이는 또 다른 문제를 야기한다.

localStorage는 데이터를 브라우저에 “영구적”으로 저장시킨다. 그래서 특정 패턴에 따라 데이터가 삭제되지 않을 수도 있다. (방어코드와 만료시간을 적용하여 해결하는 것도 가능은 하겠지만 모든 케이스에 대한 완벽 방어를 하려면 공수가 커지는 것도 고민해봐야할 문제)

sessionStorage는 localStorage와 달리 데이터를 “영구적”으로 저장하지는 않는다. localStorage 처럼 새로고침을 해도 데이터가 유지되지만, 해당 탭이 닫히면 데이터가 삭제된다. 데이터의 사용 범위도 sessionStorage가 사용된 그 ‘탭’에서만 국한된다.

이러한 특징들 때문에 localStorage, sessionStorage 모두 이번 현상을 해결하는데 적합하지 않았다.

해결 방법

목표

이 밖에도 새로운 라이브러리를 도입하는 것도 고민해 보았으나, 결국 많은 화면에서 document.cookie가 사용 적용되고 있었기에, 다음과 같은 목표를 세워두고 최대한 기존 코드를 활용하는 방법으로 수정하였다.

  1. 만료시간 지정이 가능할 것

  2. 같은 글을 여러개 탭으로 동시에 띄워도 파라미터는 독립적으로 전달될 것

  3. “파라미터 전달용 데이터”와 “검색조건 값 복원을 위한 데이터”간 충돌 혹은 덮어쓰기 현상이 발생되지 않을 것

  4. 공통함수를 구현하여 기존의 로직을 최대한 활용할 것

**구현 **

AS-IS :

  • 화면의 카테고리별로 동일한 Cookie Key 값 사용

TO-BE :

  • 생성되는 쿠키의 key값이 중복되지 않도록 조치(랜덤값)

  • 쿠키 생성 후 고유한 쿠키 key값을 url에 QueryString 형태로 저장 (화면 진입 및 뒤로가기시 key 값 전달)

  • 모든 쿠키는 독립적으로 생성되는 만큼 사용자가 여러 탭을 지속하는 만큼 쿠키가 누적되는 문제 방지 → 쿠키 생성시 현재 메모리 체크, 기준치를 초과하는 경우 오래된 쿠키 삭제

그렇게 수정된 코드는 다음과 같다.

수정된 코드

  • common.js
// 고유 key name 쿠키 생성 함수
// 여러 Tab 동시 오픈시 데이터 조회 에러 대응 > document.cookie의 비동기적 방식에 의한 현상
function setRareCookie(cookieValue, expireDate, uri) {
    checkAndDeleteCookie(); // 총 크기 체크. 기준치 이상인 경우 가장 오래된 쿠키 삭제하는 함수
    let cookieName = randomNum(); // 쿠키의 고유 key name 생성 
    changeBrowserLocation(cookieName); // key name을 URL에 설정
    document.cookie = cookieName + "=" + escape(cookieValue) + ";expires=" + expireDate + ";path=" + uri;
    return cookieName;
}

// URL에 있는 dataKey를 Key Name으로 갖는 쿠키 조회
function getRareCookie() {
    let cookieName = getDataKey();
    let cookie = document.cookie;
    if (cookie.length > 0 && !isEmpty(cookieName)) {
        let startIndex = cookie.indexOf(cookieName);
        if (startIndex != -1) {
            startIndex += cookieName.length;
            let endIndex = cookie.indexOf(";", startIndex);
            if (endIndex == -1) endIndex = cookie.length;
            return unescape(cookie.substring(startIndex + 1, endIndex));
        } else {
            return '';
        }
    } else {
        return '';
    }
}

// 쿠키 삭제
function deleteRareCookie(uri) {
    let expireDate = new Date();
    expireDate.setDate(expireDate.getDate() - 1);
    document.cookie = getDataKey() + "= " + ";expires=" + expireDate.toGMTString() + ";path=" + uri;
}

// URL의 QueryString 중 dataKey 유무 확인
function getDataKey(){
    let dataKey = new URL(location.href).searchParams.get('dataKey') || '';
    return dataKey;
}

// 화면 이동시 URL에 cookie key name 전달
function getDataKeyUrl(delimiter){
    let dataKey = getDataKey();
    return dataKey && `${delimiter}dataKey=${dataKey}`;
}

function randomNum(){
    let randomNum = String(Math.floor(Math.random() * 10000));
    let currDateTime = String(new Date().getTime()).substring(3);
    return currDateTime + randomNum;
}

// 쿠키의 총 크기 반환 함수
function getCookieSize() {
    let totalSize = 0;
    let cookies = document.cookie.split(';');
    for (let i = 0; i < cookies.length; i++) {
        let cookie = cookies[i].trim();
        totalSize += cookie.length * 2; // 쿠키의 길이 2배(유니코드 문자 고려)
    }
    return totalSize;
}

// 총 크기를 체크하여 기준치 이상인 경우 가장 오래된 쿠키 삭제
function checkAndDeleteCookie() {
    let maxSize = 8500;

    while (getCookieSize() > maxSize) {
        let cookies = document.cookie.split('; ');
        let oldestCookie = 0;
        let firstOfAll = true;
        let deleteTargetCookieName = 0;

        for (let i = 0; i < cookies.length; i++) {
            let keyName = cookies[i].substring(0, cookies[i].indexOf('='));
            if(!isNumericString(keyName)) continue;
            let keyName_dateTime = parseInt(keyName.substring(0,10));

            if(firstOfAll) {
                oldestCookie = keyName_dateTime;
                deleteTargetCookieName = keyName;
                firstOfAll = false;
                continue;
            }

            if(oldestCookie > keyName_dateTime){
                oldestCookie = keyName_dateTime;
                deleteTargetCookieName = keyName;
            }
        }

        if(firstOfAll) break; // 삭제할 쿠키 없는 경우 break
        document.cookie = deleteTargetCookieName + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
    }
}

function isNumericString(str) {
    let pattern = /^\d+$/;
    return pattern.test(str);
}

// 고유 key name을 URL에 설정
function changeBrowserLocation(dataKey){
    let params = new URLSearchParams(location.search);
    params.set('dataKey', dataKey);
    let queryString = params.toString();
    let url = location.pathname + '?' + queryString;
    window.history.replaceState({}, document.title, url);
}
  • 리스트페이지.js (저장된 쿠키 조회. 쿠키 존재하는 경우 화면에 데이터 세팅)
// 쿠키 조회 후 화면에 데이터 세팅
화면시작시실행되는초기화함수 : function(){
	this.searchCondition = getRareCookie();
	if(this.searchCondition != '') {
		...
		(화면 inputbox에 setting 변수 초기화)
		...
		deleteRareCookie("/");
	}
}

...

// 상세페이지로 이동
화면이동함수 : function(){
	...
	let dataKey = setRareCookie({다음화면에 전달할 JSON 데이터}, expireDate, "/");
	window.location.href = "{상세페이지URL}?dataKey=" + dataKey;
}
  • 상세페이지.js (뒤로가기시 파라미터 설정)
뒤로가기함수 : function() {
	window.location.href = "{목록페이지URL}" + getDataKeyUrl("?");
}
📌

Vanila JS 외에도 쿠키를 편하게 다룰수 있도록 지원하는 라이브러리

  • js-cookie
  • react-cookie