개발/Javascript

[이슈] 스네이크 케이스 <-> 카멜 케이스 변환시 성능 이슈 해결 (feat. defineProperty, laze access)

lanace 2021. 5. 27. 11:08

You don't know JS  를 스터디하면서 defineProperty 같은 함수는 내부적으로 쓰이는거고 서비스 개발할땐 별로 쓰일일이 없겠다 생각하고 넘겼는데, 어쩌다보니 써야하는 상황이 왔고, 나름 훌륭하게 해결한것 같아서 정리하고 공유할겸 글을 남기게 되었다.

 

이슈 상황

API 서버와 통신할때 JSON 을 통해서 데이터를 주고받는 상황이다.
이때 서버에선 스네이크 케이스를 사용하고, 클라이언트에선 카멜 케이스를 사용한다. 

이때 클라이언트에선 받아온 JSON의 key들을 카멜 케이스로 변환해주어야 하는데, 방법은 여러가지가 있다. 

  • key를 순환하면서 변경시키기
  • defineProperty 를 사용하여 laze access

내가 직접 작업한 방법은 위에 2가지 인데, 우선 별 생각 없이 key값을 순회하면서 하나씩 바꿔주면 해결될 문제였다. 

 

axios를 사용하고 있었고, interceptor 에서 response 를 순환하며 변경해주면 될것같아 다음과 같이 작업하였다.

// axios 인스턴스 생성시 response가 왔을때 변환
const axiosInstance = axios.create({
  ...,

  transformResponse: [
    function (data) {
      return camelize(JSON.parse(data));
    },
  ],
  
  ...
});

// JSON을 재귀로 순회하면서 표기법 변환
export const camelize = (data: any) => traverseKey(data, snakeToCamel);

// 스네이크 케이스 -> 카멜 케이스
export function snakeToCamel(string: string) {
  return string.replace(/(_\w)/g, function (m) {
    return m[1].toUpperCase();
  });
}

 

문제는 매우 큰 JSON이 왔을때 너무 많은 순환을 해야했고, 전체적인 성능 저하로 이어졌다. 

 

1차 접근 - 캐시

먼저 접근했던 방법은 캐시하는 방법이였다. 

서버와의 통신을 최대한 줄이고, key값을 미리 저장하여 변환하는데 걸리는 시간을 줄이는 것이였다. 

결과부터 말하자면 서버와 통신을 줄이는건 어느정도 효과가 있었지만 key값을 저장하는건 별 의미가 없었다. 

 

성능 저하가 발생하는 부분은 response의 JSON을 변환하는 부분이였기 때문이다.

따라서 JSON 통신하는 양을 줄이는건 의미가 조금 있었지만 JSON을 변환하는건 그대로기 때문에 큰 효과를 보진 못했다.

 

  • 서버와의 통신을 최소화 + 캐시 (약간 도움)
  • key 값을 캐시 (별 도움 안됨)

 

 

2차 접근 - defineProperty 를 통한 lazing access

방향을 바꾸어 서버에서 받아온 정보를 전부 치환해서 사용하는것은 불가능하니 데이터에 접근할때 찾아가도록 하면 해결할 수 있지 않을까 하는 생각이 들었다. 

즉, laze loading 하는것이다. 

 

어떤 값에 접근하려 할때 함수를 통해서 계산하고, 계산한 값을 주는것이다. 

obj.value1 이 있을때 일반적인 경우엔 value1에 값이 들어있지만 내 아이디어는 value1 이 함수인 것이고, value1에 접근하는 순간 함수가 실행되며 value1의 진짜 값을 찾는것이다. 

 

만약 C#에 있는 getter/setter 의 개념을 알고있다면 쉬울것같다. (사실 나도 C#은 잘 모르지만... 이런게 있었던것같아서...)

이렇게 구현하려고 이것저것 찾다보니 defineProperty가 생각났다.

 

 


defineProperty 란?

Object.defineProperty 는 Object 의 Prototype에 새로운 속성을 추가할때 사용하는 정적 메소드이다.

const object1 = {};

Object.defineProperty(object1, 'property1', {
  value: 42,
  writable: false
});

object1.property1 = 77;
// throws an error in strict mode

console.log(object1.property1);
// expected output: 42

MDN 에 있는 간단한 예제를 보면 쉽게 이해할 수 있을것같다. 

object1 이라는 객체에 property1 이라는 속성을 추가했다. 

그리고 object1.property1 로 접근하면 새로 지정한 값에 접근할 수 있는것을 살펴볼 수 있다.

 

이처럼 Object.defineProperty 는 객체에 새로운 속성을 추가해주는 메서드이다.


 

구현

그럼 이제 위 내용으로 실제 구현한 코드를 보자.

JSON 의 1차 자식들을 돌면서 카멜케이스로 된 property를 추가한다. 

const caseObjectCreator = (object: any): any => {
  const _traverse = (o: any) => caseObjectCreator(o);

  const _traverseKey2 = (obj: any) => {
    if (_.isArray(obj)) {
      return _.map(obj, _traverse);
    }

    if (_.isObject(object)) {
      const keys = Object.keys(object);
      keys.map(key => {
        if (key.indexOf('_') >= 0) {
          Object.defineProperty(object, snakeToCamel(key), {
            set: value => {},
            get: () => _traverse(obj[key])
          });
        } else {
          obj[key] = _traverse(obj[key]);
        }
      });
      return obj;
    }

    return obj;
  };

  return _traverseKey2(object);
};

 

여기서 중요하게 보아야 할 부분은 다음 코드이다.

Object.defineProperty(object, snakeToCamel(key), {
  set: value => {},
  get: () => _traverse(obj[key])
});

key를 순회하면서 Object.defineProperty 를 통해 새로운 필드를 정의한다. 

object에 key값을 카멜케이스로 변환한 속성을 추가하고, 해당 속성에 접근할때 _traverse 로 계산하도록 하는것이다. 

 

예를들면 다음과 같은 코드를 보자

const obj = {
	message_value: "hi",
	age_value: 20
}

Object.defineProperty(obj, 'messageValue', {
	get: () => obj.message_value,
	set: () => {}
});

obj.message_value;	// "hi"
obj.age_value;		// 20

obj.messageValue;	// "h1"

 

이처럼 messageValue 에 접근하려 할때 get 함수를 통해 가져오면서 obj.message_value에 접근하도록 하는것이다. 

 

이 방법을 사용하면 서버에서 JSON을 받아올때 1차 자식들만 property를 추가한다. 

const jsonObj = {
	"val_1" : 1,
	"val_2" : 2,
	"val_3" : {
    	sample_obj: {},
        sample_array: [],
        sample_number: 0
    },
}

 위와같은 Object가 있을때 위 함수를 거치면 다음과 같은 속성들이 생기는 것이다.

jsonObj.val_1;	// 1
jsonObj.val1;	// getter Function 

jsonObj.val_2;	// 2
jsonObj.val2;	// getter Function 

jsonObj.val_3;	// {...}
jsonObj.val3;	// getter Function 

jsonObj.val3.sampleObj;	// jsonObj.val3의 getter 함수 실행 후 해당 object의 smapleObj 속성 접근

 

 

 

3차 접근

또다른 문제가 생겼다.

리스트 예외처리가 안되는 것이다. 

 

이럴땐 defineProperty 에 있는 옵션을 주면 되는데, enumerable과 writable 옵션을 주면 된다. 

Object.defineProperty(object, snakeToCamel(key), {
	set: value => {},
	get: () => _traverse(obj[key]),
	enumerable: true,
	writable: true,
});

 

이렇게 옵션을 주면 리스트로 참고하여 map이나 filter 같은 순회도 정상적으로 잘 동작될 것이다. 

 

결론

성능 이슈일때 여러가지 해결 방법이 있다. 

그중 laze loading 을 통해 해결하는 방법을 선택하였고, 그 방법을 구현하기 위해 Object.defineProperty 를 사용해보았다. 

 

전체 코드

const axiosInstance = axios.create({
  transformResponse: [
    function (data) {
      try {
        if (data) {
          try {
            return caseObjectCreator(JSON.parse(data));
          } catch (error) {
            console.error(error);
            return camelize(JSON.parse(data));
          }
        }
      } catch (e) {
        return data;
      }

      return null;
    },
  ],
});

const caseObjectCreator = (object: any): any => {
  const _traverse = (o: any) => caseObjectCreator(o);

  const _traverseKey2 = (obj: any) => {
    if (_.isArray(obj)) {
      return _.map(obj, _traverse);
    }

    if (_.isObject(object)) {
      const keys = Object.keys(object);
      keys.map(key => {
        if (key.indexOf('_') >= 0) {
          Object.defineProperty(object, snakeToCamel(key), {
            set: value => {},
            get: () => _traverse(obj[key]),
            enumerable: true,
            writable: true,
          });
        } else {
          obj[key] = _traverse(obj[key]);
        }
      });
      return obj;
    }

    return obj;
  };

  return _traverseKey2(object);
};

export const camelize = (data: any) => traverseKey(data, snakeToCamel);

export function snakeToCamel(string: string) {
  return string.replace(/(_\w)/g, function (m) {
    return m[1].toUpperCase();
  });
}

 

 

참조

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty