Object.freeze, RegExp.test 버그, fetch와 axios 에러 핸들링 차이

seo young park·2023년 1월 8일
2

1️⃣ Immutable한 객체 만들기

자바스크립트의 const는 상수를 지정할 때 사용한다. 일반적으로 상수는 재선언과 재할당이 불가능하다. 그러나 const Object의 경우, 객체가 할당되는 게 아니라 객체 데이터를 가리키는 메모리 주소값이 할당되기 때문에 key-value를 추가하거나 변경하는 행위가 가능하다. 이 때, Object.freeze()를 사용하여 객체를 동결시킬 수 있다. 그 전에 객체의 속성 설명자에 대해서 알아보자.

객체의 속성 설명자

객체에는 다음과 같은 속성들이 있다.

Object.getOwnPropertyDescriptor(obj, prop) 메서드를 사용하면 객체의 속성을 확인할 수 있다.

const user = {
	name: 'Kim Minsu',
    age: 33,
  	nickname: 'kingsu',
    
}

value, writable, enumerable, configurable 등 속성 설명자가 존재하며 기본값은 true다.

  • writable은 속성 값의 변경 가능 여부를 나타낸다.
  • enumerable은 속성의 열거 가능 여부를 나타낸다. false로 설정되면 Object.keysfor-in등의 메서드를 사용한 열거가 불가능해진다.
  • configurable은 속성 재설정 가능 여부를 나타난다. enumerable,configurable을 다시 설정할 수 없으며 값을 삭제할 수 없다. 다만 writabletrue인 경우, 값을 변경하거나 writablefalse로 변경하는 것이 가능해진다.

그렇다면 Object.seal()과 Object.freeze()를 사용하면 객체의 속성이 어떻게 바뀌는지, 프로퍼티 변경/제거/추가를 기준으로 비교해보자.

Object.seal()과 Object.freeze()

Object.seal(obj)

Object.seal은 객체를 밀봉하여 프로퍼티를 추가하거나 제거할 수 없다. 그리고 configurable 속성이 false가 되어 속성을 재설정할 수 없게 만든다. 그러나 writable 속성은 true를 유지하고 있기 때문에 값을 변경할 수 있다.

  • 프로퍼티 변경 ✅
  • 프로퍼티 제거 ❌
  • 프로퍼티 추가 ❌
const user = {
	name: 'Kim Minsu',
  age: 33,
  nickname: 'kingsu',   
};

//프로퍼티 변경
user.name = 'Park Minsu';
// { name: 'Park Minsu', age: 33, nickname: 'kingsu' }


//프로퍼티 제거
delete user.age;
// { name: 'Park Minsu', age: 33, nickname: 'kingsu' }


//프로퍼티 추가
user.address = 'Seoul';
// { name: 'Park Minsu', age: 33, nickname: 'kingsu' }

Object.freeze(obj)

Object.freeze는 객체를 동결시킨다. writableconfigurablefalse가 되어, 프로퍼티를 변경/제거/추가할 수 없다.

  • 프로퍼티 변경 ❌
  • 프로퍼티 제거 ❌
  • 프로퍼티 추가 ❌
const user = {
	name: 'Kim Minsu',
  age: 33,
  nickname: 'kingsu',   
};

Object.freeze(user);

//프로퍼티 변경
user.name = 'Park Minsu';
{ name: 'Kim Minsu', age: 33, nickname: 'kingsu' }


//프로퍼티 제거
delete user.age;
{ name: 'Kim Minsu', age: 33, nickname: 'kingsu' }


//프로퍼티 추가
user.address = 'Seoul';
{ name: 'Kim Minsu', age: 33, nickname: 'kingsu' }

예외 - 중첩된 객체

Object.seal()Object.freeze() 모두 중첩된 객체까지 밀봉하거나 봉인할 수 없다. 아래 예시를 보면 Object.freeze()를 사용하였음에도 중첩된 객체의 writableconfigurabletrue다.

const user = {
	fullName: {
    firstName: 'Minsu',
    lastName: 'Kim'
  },
  age: 33,
  nickname: 'kingsu',
};

Object.freeze(user);

user.fullName.lastName = 'Park';

// {
  fullName: { firstName: 'Minsu', lastName: 'Park' },
  age: 33,
  nickname: 'kingsu'
}

MDN에서는 다음과 같은 deepFreeze 메서드를 만들어 사용할 것을 권하고 있다.

function deepFreeze(object) {
  // Retrieve the property names defined on object
  const propNames = Object.getOwnPropertyNames(object);

  // Freeze properties before freezing self
  for (const name of propNames) {
    const value = object[name];

    if ((value && typeof value === "object") || typeof value === "function") {
      deepFreeze(value);
    }
  }

  return Object.freeze(object);
}

2️⃣ RegExp.prototype.test()

정규표현식에는 주어진 문자열과 일치여부를 판별하는 test 메서드가 있다. URL 추출 기능 구현 중 이 test 메서드의 버그를 발견했다.

const regexAbsoluteUrl = /\bhttps?:\/\/\S+/gi;

const url = 'https://naver.com';


console.log(regexAbsoluteUrl.test(url)); //true
console.log(regexAbsoluteUrl.test(url)); //false

이는 정규표현식 객체가 stateful이기 때문에 가능하다. 정규표현식에 전역 플래그를 설정한 경우, test()메서드를 호출할 때마다 lastIndex값을 업데이트하고 이전 일치된 내용은 건너뛰고 진행한다. test()메서드가 false를 반환할 때 lastIndex는 초기화된다.

const pattern = new RegExp("te", "gi");
const stringOne="test";

pattern.lastIndex; // 0
pattern.test(stringOne); // true
pattern.lastIndex; // 2
pattern.test(stringOne); // false, 일치하는 값이 없으므로 초기화
pattern.lastIndex; //0

주의할 점은 test()메서드가 true를 반환하면, 다른 문자열이 인자로 주어져도 lastIndex값이 초기화되지 않는다.

const pattern = new RegExp("te", "gi");
const stringOne="test";
const stringTwo="te2st";

pattern.lastIndex; // 0
pattern.test(stringOne); // true
pattern.lastIndex; // 2
pattern.test(stringTwo); // false, 일치하는 값이 없으므로 초기화
pattern.lastIndex; //0

이를 해결하기 위해선 전역 플래그를 사용하지않거나 String.match(regexp) 혹은 String.search(regexp)를 사용할 수 있겠다.

stringOne.match(pattern); // [ 'te' ]
stringOne.search(pattern); // 0

stringOne.match(pattern).length > 0; // true
stringOne.search(pattern) > -1; // true

3️⃣ fetch vs axios

Next의 API Routes를 활용하여 기능을 구현하던 도중, fetch와 axios의 에러 처리 방식이 다르다는 것을 알게 되었다.

axios는 status code가 2xx의 범위를 넘으면 reject 한다.
그런데 fetch의 promise는 2xx의 범위를 넘으면 ok 필드를 false로 설정할뿐 reject하지 않는다. 서버에서 헤더를 포함한 응답을 받으면 정상적으로 이행한다. 네트워크 장애가 발생하거나, 요청을 완료하지 못하는 경우에만 reject 한다.

따라서 fetch를 사용할거라면, then절에서 response의 ok필드가 false인 경우 커스텀 에러가 발생하도록 아래와 같이처리해주어야한다.

  await fetch(url, {
        method: 'POST',
      }).then(res => {
        if (!res.ok) {
          throw new Error(`${res.status}`);
        } 
    ...
  }.catch(() => {
  	...
  })

1개의 댓글

comment-user-thumbnail
2023년 1월 9일

우와 굉장히 잘 정리된 글이군요!!

답글 달기