자바스크립트 Deep Equal 구현

jjyung·2021년 9월 22일
1

JS

목록 보기
4/13

자바스크립트에서는 값의 일치여부를 체크하다보면 예상치못한 결과를 마주할때가 있다.

예를 들어 0과 -0을 비교해보았을 때 false가 나와야 정상(?)이지만 일치 연산자로 비교했을때 true가 나온다. 또한, NaN은 자기자신과 일치 연산자로 비교했을때 false가 나오는 값중에 하나이다.

배열과 객체에서도 이러한 현상이 발생한다.

const a = { a: 1, b: { c: 2, d: { e: 3 } } }
const b = { b: { d: { e: 3, f: 4 }, c: 2 }, a: 1 }

이 예시를 본다면 객체 안에 객체, 그리고 그 안에 또 객체가 존재한다. 이런 내부 객체까지 확인해야하지만, 일치연산자만으로는 부족하다.

이 문제를 해결하고자 isEqual이라는 함수를 구현해보았다.


1. 문제 세분화

1) 오류 해결하기

  • 자바스크립트는 매개변수와 인수의 개수가 동일하지않아도 오류가 나지 않는다. 만약 인수가 부족하다면 오류 메세지를 날려주도록 설정하기로 했다.

2) 객체와 원시값일경우 세분화하기

  • 객체값일때와 원시값일때를 나누어주고자했다. 원시값과 객체일때 타입체크를 조금 더 편하게 하기 위해서 둘로 나누어 접근했다.

3) 객체 내부에서도 세분화

  • 객체 내부에서도 비교 방식은 다르다. 가령, 배열과 객체({})일때는 객체 내부의 객체까지 비교해주어야했기 때문에 길이비교와 Object.keys()를 통해서 내부까지 비교하고자했다. 반면, 일반 객체, 배열 외의 객체등은 단순 일치 연산자로 비교해도 원하는 값이 도출되었기때문에 이 둘을 나누어 처리했다.

2. 문제 해결

1) 화살표 함수 활용하기

  • 화살표 함수를 이용하여 원시값인지 확인할 수 있는 함수 리터럴과 객체{}인지 확인할 수 있는 함수 리터럴을 만들었다. 이렇게 두 개의 함수 리터럴을 만든 이유는 나중에 조건 비교할 때 가독성이 더 좋아지게 하기 위해서다. 이렇게 식별자 이름을 알기 쉽게 설정함으로써 보는 사람들이 이 함수가 어떤 역할을 하는지 알 수 있기 때문이다.
  • 또한, 함수로 따로 설정하지않자, if문 내부에 들어가는 조건이 너무 길어져 ESLint에서 오류라고 나왔다. max길이는 110인데 나는 116까지 넘어갔기때문에, 이 문제를 해결하기위해 함수 리터럴을 만들었다.
  const isPrimitive = obj => obj !== Object(obj);
  const isObject = obj => obj.constructor === Object;

2) []{} 일때와 아닐 경우 세분화하기.

  • 앞서 말했다싶이 배열과 객체{}인 경우는 for문과 length를 비교해주어야하는 반면, 빌트인 함수는 단순 일치 연산자로 비교해도 값이 나온다.
// []와 {}가 아닐때
    if (!isObject(obj1) && !isObject(obj2) &&
      !Array.isArray(obj1) && !Array.isArray(obj2)){
      if (obj1 !== obj2) return false;
    }
  • []{}일때, 두 비교하는 객체가 같은 배열이 아니거나 같은 타입이 아닐 경우도 세분화해서 구현했다.
// [] 또는 {}일때
    if (
      (Array.isArray(obj1) && !Array.isArray(obj2)) ||
      (!Array.isArray(obj1) && Array.isArray(obj2)))
      return false;
    if (Object.keys(obj1).length !== Object.keys(obj2).length) return false;
    for (const key in obj1) {
      if (!(key in obj2)) return false;
    }
    for (const key in obj1) {
      if (!isEqual(obj1[key], obj2[key])) return false;
    }
  }
  return true;
}

3. 1차 결론

1. 결과

  • branch에서 100점을 받지는 못했지만, pass는 했다.
  • 위의 문제 해결방법으로는 코드가 너무 사족이 길고 중복이 많다는 것을 깨달았다. 이런 문제점을 기반으로 개선한 코드를 올릴 예정이다.

2. 개선방법

  1. if문은 depth를 1까지로만 잡아야한다 (너무 복잡해지면 가독성도 떨어지고 복잡해지니까...) -> 먼저 걸러야 할 조건일수록 위에 적어두기.
  2. 에러처리 : 백틱을 사용해서 하나로 처리하기
  if (arguments.length < 2) {
    throw new Error(
      `isEqual requires at least 2 argument, but only ${arguments.length} were passed`
    );
  }
  1. for...in문 사용하지 않고 리펙토링 하기 (for...of나 Object.keys사용하기)
  2. 리펙토링 하면서 몰랐던 부분은 주석으로 달아두기
  3. !== 보다는 === 사용하기 (부정은 일관성있게 사용하지 않는 편이 좋음)
  4. 일관성있게 작성하기 (not을 사용할꺼면 not만 사용하기)
  5. 변수명 개선하기 (이해하기 쉽게 의미를 내포하는 변수명 사용, 긍정형 변수명 사용하기)

4. 리펙토링

1. Jest results

  • 우선 branch에서 걸린 라인은 사용되지 않고 있기때문에 제거해준후 npm을 돌려보니 점수가 100점이 나왔고, 문제없이 pass도 했다.

2. 변수명

매개변수명도 obj에서 comparison으로 바꾸어줬다.
이유 1) 객체 타입만 받아오는것이아니라 원시 타입도 받아오기때문에 obj는 옳지 않다고 판단하였다.
이유 2) input을 변수명도 고려했었지만, 이 변수명은 두 개를 비교대상으로 받아온다는 의미를 내포하지못해 사용하지않았다.

3. 조건

1) 에러

  • 에러를 두 경우로 나누었지만, 이는 중복이 반복되어 비효율적이다. 그래서 한 문장으로 합쳤고, 동적으로 변수를 받아오는 방식으로 바꾸었다.
  if (arguments.length < 2) {
    throw new Error(
      `isEqual requires at least 2 argument, but only ${arguments.length} were passed`
    );
  }

2) 타입 체크

  • 이미 원시 타입을 체크할때 타입이 걸러졌기때문에 불필요한 문장은 제거해주었다. (중복 제거)
// 원시 타입 체크
  if (isPrimitive(comparison1) && isPrimitive(comparison2))
    return Object.is(comparison1, comparison2);

// 객체 타입 체크 -> 불필요
if ( typeof obj1 === 'object' && obj1 !== null &&
 typeof obj2 === 'object' && obj2 !== null) {...}

3) if문

  • if문의 depth를 1로 다 바꾸어주며 문장을 세분화해주었다 (먼저 걸러져야할 아이들은 맨 위로 올라가도록 했다) (if문의 depth가 깊어질수록 이해하기도 힘들어지고 복잡해진다)
  if (!isObject(comparison1) && !isObject(comparison2) && !Array.isArray(comparison1) && !Array.isArray(comparison2))
    return Object.is(comparison1, comparison2);

4) for...in 변경

  • for...in은 안티패턴이다. 그래서 Object.keys()나 for...of를 사용할 것을 권장한다. 그래서 Object.keys로 키 값만 배열로 받아와 of를 사용해 하나씩 값을 뽑아와 사용하도록 했다.
  • hasOwnProperty는 해당 객체가 스스로 정의한 프로퍼티에 대한 소유 여부를 나타낸다. 그래서 comparison2라는 객체에 key값이 존재하는지 여부를 체크하고 있는 것이다 (만약 존재하지 않는다면 false)
  • 두 번째 for문에서 재귀함수를 사용해 해당 객체의 key에 해당하는 값을 함수에 넣었을때 true가 아니라면 false를 반환하도록 하고 있다.
 for (const key of Object.keys(comparison1)) {
    if (!Object.prototype.hasOwnProperty.call(comparison2, key)) return false;
 }

  for (const key of Object.keys(comparison1)) {
    if (!isEqual(comparison1[key], comparison2[key])) return false;
  }

5. 결론

  • 리펙토링은 정말 끝이 없는것 같다. 보면 볼수록 고쳐야할 것 같은 부분이 많아지고, 고민이 깊어지는 것 같다. 강사님말씀대로 코드 구현을 1시간을 한다면 리펙토링을 2-3시간 해야하는 것 같다. 아직 부족한 점이 너무나도 많은 코드지만 계속 내것으로 만들고 공부를 해나가야겠다.
profile
🏃‍♀️movin' forward, developer

0개의 댓글