미니 프로젝트를 하는데 백엔드 서버 api의 응답 데이터 포맷이 snake_case로 되어있었다.
하지만 나는 이미 타입스크립트로 camelCase를 사용하여 타입을 정의하고 테스트를 위한 msw의 handler api들도 이미 camelCase로 사용하고 있었고 camelCase를 더 좋아한다.
이걸 하나하나 snake_case로 변경하고 싶지 않았기 때문에 일괄적으로 변환하여 처리하고 싶었다.
(1) snake_case object
에서 key
를 camelCase
로 변환해주는 함수를 만든다.
(2) axios response interceptor
에 적용해 api응답 데이터를 변환한다.
(3) 이렇게 사용하면 기존 코드를 건드리지 않아도 되고 test api도 변경하지 않아도 된다.
snake_case
키를 가진 객체를 camelCase
로 변환해야 되는데
직접 해도 되지만 좀 더 편하게 변환하고 싶었고 많은 시간을 들이고 싶지 않았다.
Lodash?
Lodash
는 자바스크립트 유틸리티 라이브러리로, 배열, 숫자, 객체, 문자열 등의 데이터를 다루는데 유용한 다양한 함수들을 제공합니다. Lodash의 함수들은 자바스크립트에서 제공하는 기본적인 함수와 비슷한 기능을 제공하지만, 더욱 간결하고 성능이 더 뛰어나며, 깊이 있는 객체의 다루기가 쉬운 등의 장점이 있습니다. 또한, IE 11을 포함한 대부분의 모던 브라우저와 Node.js에서 사용할 수 있습니다.
자바스크립트 객체를 다루기 쉽게 해주는 라이브러리인데 들어만 보았지 사용해보는 것은 처음이었다.
이번에는 일부 함수들만 사용하게 되겠지만 나중에 유용한 함수들이 무엇이 있는지 알아보고 더 사용해봐야겠다.
설치하기
yarn add lodash
사용하기
import _ from 'lodash';
_.???
으로 사용한다.
보통 본인은 자바스크립트에서 함수를 호출할 때 사용하지 않는 파라미터를 _
로 선언해 사용하는데 lodash
가 있다면 주의해야 할 것 같다.
내가 쓸 함수
isArray: 배열 여부를 판단하는 함수
isObject: 객체 여부를 판단하는 함수
mapValues: 객체의 값을 변형하는 함수
mapKeys: 객체의 키를 변형하는 함수
const obj = { a: 1, b: 2, c: 3 };
const newObj = _.mapKeys(obj, (value, key) => key.toUpperCase());
console.log(newObj); // { A: 1, B: 2, C: 3 }
camelCase: 문자열을 카멜케이스로 변경해주는 함수
함수 구현하기
import _ from 'lodash';
interface ObjectLiteral {
[key: string]: any;
}
function snakeToCamel(obj: ObjectLiteral): ObjectLiteral {
if (_.isArray(obj)) {
return obj.map((v) => snakeToCamel(v));
}
if (_.isObject(obj)) {
return _.mapValues(
_.mapKeys(obj, (v, k) => _.camelCase(k)),
(v) => snakeToCamel(v),
);
}
return obj;
}
당연하지만 재귀구조로 변환하게 만들어야 한다.
응답 객체 데이터의 깊이가 1일것이라는 보장은 없다.
배열이라면 항목마다 변환할 수 있게 재귀를 돌고
오브젝트라면 키를 변경하고 재귀를 돌며
값이라면 값을 그대로 리턴한다.
이렇게 해서 value는 유지시키고 객체의 key만 camelCase
로 변경한다.
테스트
간단하게 nested 구조나 배열이 포함되어있거나 키 말고 값이 snake_case인데 camelCase로 변경되지 않는지 정도만 테스트했다.
import snakeToCamel from '@/utils/snakeToCamel';
describe('snakeToCamel', () => {
test('snake_case 객체는 camelCase 객체로 변환되어야 한다.', () => {
const snakeCaseData = {
first_name: 'John',
last_name: 'Doe',
age: 28,
};
const expected = {
firstName: 'John',
lastName: 'Doe',
age: 28,
};
expect(snakeToCamel(snakeCaseData)).toEqual(expected);
});
test('nested snake_case 객체는 object의 key가 camelCase 객체로 모두 변환되어야 한다.', () => {
const snakeCaseData = {
user_info: {
first_name: 'John',
last_name: 'Doe',
age: 28,
},
hello_world: {
hell: 'hello_world',
},
};
const expected = {
userInfo: {
firstName: 'John',
lastName: 'Doe',
age: 28,
},
helloWorld: {
hell: 'hello_world',
},
};
expect(snakeToCamel(snakeCaseData)).toEqual(expected);
});
test('배열이 포함된 nested snake_case 객체는 object의 key가 camelCase 객체로 모두 변환되어야 한다.', () => {
const snakeCaseData = {
data: [
{
user_info: {
user_name: 'user_name',
skill_list: ['sk_1', 'sk_2', 'sK3'],
},
},
{
user_info: {
user_name: 'user_name_2',
skill_list: ['sk_1', 'sk_2', 'sK3'],
hell_world: [{ hell_world: 1234 }, { abc_hello: '1234' }],
},
},
],
};
const expected = {
data: [
{
userInfo: {
userName: 'user_name',
skillList: ['sk_1', 'sk_2', 'sK3'],
},
},
{
userInfo: {
userName: 'user_name_2',
skillList: ['sk_1', 'sk_2', 'sK3'],
hellWorld: [{ hellWorld: 1234 }, { abcHello: '1234' }],
},
},
],
};
expect(snakeToCamel(snakeCaseData)).toEqual(expected);
});
});
클라이언트 사이드에서 사용되는 모든 api를 axios로 호출하는 형태라 응답 인터셉터에 적용해서 쉽게 변환해서 사용할 수 있다.
function getObject(response: AxiosResponse) {
const { data } = response;
return data !== null && typeof data === 'object' ? snakeToCamel(response.data) : {};
}
instance.interceptors.response.use(
(response) => getObject(response),
(error) => genErrorResponse(error),
);
이제 원래 하던대로 테스트 api를 사용하여 개발하면 되고 실제 백엔드 api를 연결해서 사용할 때도 똑같이 camelCase를 사용할 수 있게 되었다.
재귀를 돌리기 때문에 응답 데이터 케이스가 엄청 많을 경우 변환 자체가 성능에 영향을 주는지는 추후 테스트해보아야 할 것 같다.
이렇게 기존의 코드에는 변경없이 쉽게 포맷을 변환해서 사용할 수 있게 되었다!