JavaScript 개발자를 위한 10가지 중요한 Lodash 기능

JavaScript 개발자에게 Lodash는 익숙한 이름입니다. 하지만, 라이브러리가 방대하여 때로는 사용하기 어렵다고 느껴질 수 있습니다. 이제, 더 이상 그럴 필요가 없습니다!

Lodash, Lodash, Lodash… 어디서부터 시작해야 할까요? 🤔

과거 JavaScript 생태계는 혼란스러웠습니다. 마치 서부 개척 시대나 정글처럼, 많은 일들이 일어났지만, 개발자들의 일상적인 어려움과 생산성 향상에 대한 해결책은 거의 없었습니다.

그때 Lodash가 등장했고, 모든 것이 정리된 듯한 느낌을 주었습니다. 정렬과 같은 단순한 작업부터 복잡한 데이터 구조 변환에 이르기까지, Lodash는 JavaScript 개발자의 삶을 행복으로 바꿔줄 다양한 기능을 제공했습니다.

Lodash의 등장이었습니다!

현재 Lodash는 어떤 위치에 있을까요? 여전히 초기 제공했던 모든 장점을 가지고 있지만, JavaScript 커뮤니티에서 예전만큼 환영받지 못하는 듯합니다. 그 이유는 무엇일까요? 몇 가지 이유를 생각해 볼 수 있습니다.

  • Lodash 라이브러리의 일부 함수는 큰 목록에서 사용 시 속도가 느렸습니다. 대부분의 프로젝트(95%)에는 영향이 없었지만, 나머지 5%의 영향력 있는 개발자들이 Lodash에 대한 부정적인 인식을 퍼뜨렸고, 그 효과가 커뮤니티 전체에 영향을 미쳤습니다.
  • JavaScript 생태계에는 자만심이 필요 이상으로 만연한 경향이 있습니다(Golang 커뮤니티에 대해서도 마찬가지일 수 있습니다). 따라서 Lodash와 같은 라이브러리에 의존하는 것을 어리석다고 생각하며, 사람들이 그러한 해결책을 제안할 때 StackOverflow와 같은 포럼에서 비난받습니다(“뭐라고?! 전체 라이브러리를 사용한다고? `filter()`와 `reduce()`를 조합하면 간단한 기능으로 똑같이 할 수 있어!”).
  • Lodash는 구식입니다. 적어도 JavaScript 표준을 기준으로 보면 그렇습니다. 2012년에 출시되었으므로, 글을 쓰는 시점에는 거의 10년이 되었습니다. API는 안정적이었고, 매년 새로운 기능을 추가할 필요가 없었습니다.

제 생각에는 Lodash를 사용하지 않는 것은 JavaScript 코드베이스에 큰 손실입니다. Lodash는 실무에서 발생하는 일상적인 문제에 대해 버그가 없고 우아한 해결책을 제공하며, 이를 사용하면 코드를 더 쉽게 읽고 유지 관리할 수 있습니다.

이제, 몇 가지 일반적인 (혹은 특별한) Lodash 기능을 살펴보고, 이 라이브러리가 얼마나 유용하고 멋진지 확인해 봅시다.

깊은 복제

JavaScript에서 객체는 참조로 전달되므로, 새로운 데이터 세트를 만들려고 할 때 개발자들에게 어려움을 줄 수 있습니다. 데이터를 복제하면 원본 데이터가 변경될 수 있다는 문제가 발생하기 때문입니다.

let people = [
  {
    name: 'Arnold',
    specialization: 'C++',
  },
  {
    name: 'Phil',
    specialization: 'Python',
  },
  {
    name: 'Percy',
    specialization: 'JS',
  },
];

// C++ 개발자 찾기
let folksDoingCpp = people.filter((person) => person.specialization == 'C++');

// JS로 변환
for (person of folksDoingCpp) {
  person.specialization = 'JS';
}

console.log(folksDoingCpp);
// [ { name: 'Arnold', specialization: 'JS' } ]

console.log(people);
/*
[
  { name: 'Arnold', specialization: 'JS' },
  { name: 'Phil', specialization: 'Python' },
  { name: 'Percy', specialization: 'JS' }
]
*/

원래 `people` 배열이 변경된 것을 볼 수 있습니다(Arnold의 전문 분야가 C++에서 JS로 변경됨). 이는 소프트웨어 시스템의 무결성에 큰 영향을 미칩니다! 따라서 원본 배열의 실제 (깊은) 복사본을 만드는 방법이 필요합니다.

이것이 JavaScript에서 “어리석은” 코딩 방식이라고 주장할 수 있습니다. 하지만 실제로는 다소 복잡합니다. 구조 분해 할당을 사용할 수 있지만, 복잡한 객체와 배열을 구조 분해하려고 시도한 사람이라면 그 어려움을 알 것입니다. JSON을 사용한 직렬화 및 역직렬화가 깊은 복사를 달성하는 또 다른 방법이지만, 코드가 더욱 복잡해집니다.

반면, Lodash를 사용하면 솔루션이 얼마나 우아하고 간결한지 볼 수 있습니다.

const _ = require('lodash');

let people = [
  {
    name: 'Arnold',
    specialization: 'C++',
  },
  {
    name: 'Phil',
    specialization: 'Python',
  },
  {
    name: 'Percy',
    specialization: 'JS',
  },
];

let peopleCopy = _.cloneDeep(people);

// C++ 개발자 찾기
let folksDoingCpp = peopleCopy.filter(
  (person) => person.specialization == 'C++'
);

// JS로 변환
for (person of folksDoingCpp) {
  person.specialization = 'JS';
}

console.log(folksDoingCpp);
// [ { name: 'Arnold', specialization: 'JS' } ]

console.log(people);
/*
[
  { name: 'Arnold', specialization: 'C++' },
  { name: 'Phil', specialization: 'Python' },
  { name: 'Percy', specialization: 'JS' }
]
*/

깊은 복사 후 `people` 배열이 변경되지 않았음을 알 수 있습니다(Arnold는 여전히 C++ 전문가입니다). 더 중요한 것은 코드를 이해하기 쉽다는 것입니다.

배열에서 중복 제거

배열에서 중복 항목을 제거하는 것은 인터뷰/화이트보드 문제에 자주 등장합니다. 이 작업을 직접 수행하는 사용자 정의 함수를 작성할 수도 있지만, 배열을 고유하게 만드는 다양한 상황에 직면하면 어떻게 해야 할까요? 여러 가지 사용자 정의 함수를 작성하거나, Lodash를 사용할 수 있습니다!

단순한 배열에서 중복을 제거하는 첫 번째 예는 Lodash의 속도와 안정성을 잘 보여줍니다. 이 작업을 직접 코딩한다고 상상해 보세요!

const _ = require('lodash');

const userIds = [12, 13, 14, 12, 5, 34, 11, 12];
const uniqueUserIds = _.uniq(userIds);
console.log(uniqueUserIds);
// [ 12, 13, 14, 5, 34, 11 ]

결과 배열이 정렬되지 않았다는 점에 주목하십시오. 물론, 여기서는 문제가 되지 않습니다. 하지만 더 복잡한 시나리오를 생각해 봅시다. 어딘가에서 가져온 사용자 배열이 있지만, 이 배열에 고유한 사용자만 포함되어 있는지 확인해야 합니다. Lodash를 사용하면 간단합니다!

const _ = require('lodash');

const users = [
  { id: 10, name: 'Phil', age: 32 },
  { id: 8, name: 'Jason', age: 44 },
  { id: 11, name: 'Rye', age: 28 },
  { id: 10, name: 'Phil', age: 32 },
];

const uniqueUsers = _.uniqBy(users, 'id');
console.log(uniqueUsers);
/*
[
  { id: 10, name: 'Phil', age: 32 },
  { id: 8, name: 'Jason', age: 44 },
  { id: 11, name: 'Rye', age: 28 }
]
*/

이 예제에서는 `uniqBy()` 메서드를 사용하여 객체의 `id` 속성을 기준으로 중복을 제거하라고 Lodash에 지시했습니다. 이 간단한 코드는 수십 줄의 코드와 버그 발생 가능성을 줄여줍니다!

Lodash에는 고유한 배열을 만드는 데 사용할 수 있는 더 많은 방법이 있습니다. 문서를 참고하세요.

두 배열의 차이점

합집합, 차집합 등은 집합론 수업에서 들어봤을 법한 용어이지만, 일상적인 개발 작업에서도 자주 사용됩니다. 두 목록을 비교하여 고유한 항목을 찾거나 두 목록을 병합해야 하는 경우가 종종 있습니다. 이러한 상황에서 `difference` 함수는 유용합니다.

간단한 시나리오로 시작해 보겠습니다. 시스템의 모든 사용자 ID 목록과 활성화된 사용자 ID 목록을 가지고 있다고 가정해 봅시다. 비활성 사용자 ID는 어떻게 찾을 수 있을까요? 간단하죠?

const _ = require('lodash');

const allUserIds = [1, 3, 4, 2, 10, 22, 11, 8];
const activeUserIds = [1, 4, 22, 11, 8];

const inactiveUserIds = _.difference(allUserIds, activeUserIds);
console.log(inactiveUserIds);
// [ 3, 2, 10 ]

만약 실제 개발 환경에서처럼 객체 배열로 작업을 해야 한다면 어떻게 될까요? Lodash에는 `differenceBy()` 메서드가 있습니다!

const allUsers = [
  { id: 1, name: 'Phil' },
  { id: 2, name: 'John' },
  { id: 3, name: 'Rogg' },
];
const activeUsers = [
  { id: 1, name: 'Phil' },
  { id: 2, name: 'John' },
];
const inactiveUsers = _.differenceBy(allUsers, activeUsers, 'id');
console.log(inactiveUsers);
// [ { id: 3, name: 'Rogg' } ]

깔끔하죠?!

`difference`와 마찬가지로 Lodash에는 합집합, 교집합 등과 같은 다른 집합 연산을 위한 메서드도 있습니다.

배열 병합

배열을 병합해야 할 필요성은 자주 발생합니다. 예를 들어, API 응답을 받고 중첩된 객체/배열의 복잡한 목록에 `map()` 및 `filter()`를 적용하여 사용자 ID 목록을 추출해야 할 때 배열의 배열을 처리해야 합니다. 다음은 이 상황을 보여주는 코드 스니펫입니다.

const orderData = {
  internal: [
    { userId: 1, date: '2021-09-09', amount: 230.0, type: 'prepaid' },
    { userId: 2, date: '2021-07-07', amount: 130.0, type: 'prepaid' },
  ],
  external: [
    { userId: 3, date: '2021-08-08', amount: 30.0, type: 'postpaid' },
    { userId: 4, date: '2021-06-06', amount: 330.0, type: 'postpaid' },
  ],
};

// 후불 주문을 한 사용자 ID 찾기
const postpaidUserIds = [];

for (const [orderType, orders] of Object.entries(orderData)) {
  postpaidUserIds.push(orders.filter((order) => order.type === 'postpaid'));
}
console.log(postpaidUserIds);

이제 `postPaidUserIds`가 어떻게 생겼는지 추측할 수 있습니까? 힌트: 엉망입니다!

[
  [],
  [
    { userId: 3, date: '2021-08-08', amount: 30, type: 'postpaid' },
    { userId: 4, date: '2021-06-06', amount: 330, type: 'postpaid' }
  ]
]

이제, 현명한 개발자라면 주문 객체를 추출하여 하나의 배열로 만드는 사용자 정의 논리를 작성하고 싶지 않을 것입니다. Lodash의 `flatten()` 메서드를 사용하고 간단하게 해결하세요.

const flatUserIds = _.flatten(postpaidUserIds);
console.log(flatUserIds);
/*
[
  { userId: 3, date: '2021-08-08', amount: 30, type: 'postpaid' },
  { userId: 4, date: '2021-06-06', amount: 330, type: 'postpaid' }
]
*/

`flatten()`은 한 단계만 깊이로 처리합니다. 즉, 객체가 2단계, 3단계 또는 그 이상 깊이에 있다면 `flatten()`은 원하는 결과를 제공하지 않습니다. 이러한 경우 Lodash에는 `flattenDeep()` 메서드가 있지만, 매우 큰 구조에 적용하면 성능이 저하될 수 있습니다. (내부적으로 재귀적으로 작동하기 때문입니다.)

객체/배열이 비어 있습니까?

JavaScript에서 “거짓” 값과 유형이 작동하는 방식 때문에, 때로는 비어 있는지 확인하는 것과 같은 간단한 작업이 어려울 수 있습니다.

배열이 비어 있는지 어떻게 확인할 수 있을까요? 길이를 확인하면 됩니다. 이제 객체가 비어 있는지 어떻게 확인할 수 있을까요? `[] == false`와 `{} == false`라는 JavaScript의 특성 때문에 혼란스러워질 수 있습니다. 코드를 작성할 때, 이러한 문제들은 코드 유지 보수를 어렵게 만들 수 있습니다.

누락된 데이터 처리

현실 세계에서 데이터는 항상 우리가 원하는 대로 제공되지 않습니다. 간결하고 일관된 데이터가 제공되는 경우는 드뭅니다. 한 가지 일반적인 예는 API 응답으로 수신된 큰 데이터 구조에서 `null` 객체/배열이 누락된 경우입니다.

다음 객체를 API 응답으로 받았다고 가정합니다.

const apiResponse = {
  id: 33467,
  paymentRefernce: 'AEE3356T68',
  // `order` 객체 누락
  processedAt: `2021-10-10 00:00:00`,
};

보통 API 응답에서 `order` 객체를 받지만, 항상 그런 것은 아닙니다. 이 객체에 의존하는 코드가 있다면 어떻게 할까요? 한 가지 방법은 방어적으로 코딩하는 것이지만, `order` 객체가 중첩된 정도에 따라 런타임 오류를 피하려면 곧 코드가 복잡해질 것입니다.

if (
  apiResponse.order &&
  apiResponse.order.payee &&
  apiResponse.order.payee.address
) {
  console.log(
    'The order was sent to the zip code: ' +
      apiResponse.order.payee.address.zipCode
  );
}

🤢🤢 이 코드는 작성하기도, 읽기도, 유지 관리하기도 어렵습니다. 다행히도 Lodash에는 이러한 상황을 처리하는 간단한 방법이 있습니다.

const zipCode = _.get(apiResponse, 'order.payee.address.zipCode');
console.log('The order was sent to the zip code: ' + zipCode);
// The order was sent to the zip code: undefined

누락된 값에 대해 `undefined`를 반환하는 대신 기본값을 제공하는 옵션도 있습니다.

const zipCode2 = _.get(apiResponse, 'order.payee.address.zipCode', 'NA');
console.log('The order was sent to the zip code: ' + zipCode2);
// The order was sent to the zip code: NA

`get()` 메서드는 코드를 단순하게 만들어 주는 기능 중 하나입니다. 화려한 기능은 아니지만, 얼마나 많은 고통을 줄여줄 수 있는지 생각해 보세요! 😇

디바운싱

디바운싱은 프런트엔드 개발에서 흔히 사용되는 개념입니다. 이 개념은 때때로 작업을 즉시 시작하지 않고 일정 시간(일반적으로 몇 밀리초) 후에 시작하는 것이 유익하다는 것입니다.

검색 창이 있는 전자 상거래 웹 사이트를 예로 들어보겠습니다. 검색어를 기반으로 제안/미리보기를 표시할 때, 사용자가 Enter 키를 누르거나 “검색” 버튼을 클릭하기를 기다리고 싶지 않을 것입니다. 그러나 분명한 해결책은 문제가 있습니다. 검색 창의 `onChange()` 이벤트에 이벤트 리스너를 추가하고, 모든 키 입력에 대해 API 호출을 실행하면 백엔드에 문제가 발생합니다. 불필요한 API 호출이 너무 많고(예를 들어 “white carpet brush”를 검색하면 총 18개의 요청이 발생합니다!), 대부분의 사용자 입력이 완료되지 않았기 때문에 관련성이 없습니다.

디바운싱이 해결책입니다. 텍스트가 변경되는 즉시 API 호출을 보내지 않고, 일정 시간(예: 200밀리초) 동안 기다린 다음, 그 시간 내에 다른 키 입력이 있으면 이전 타이머를 취소하고 다시 기다리기를 시작합니다. 결과적으로 API 요청을 백엔드로 보내는 시점은 사용자가 잠시 멈출 때입니다. (사용자가 생각하거나 입력을 완료했을 때, 응답을 기대할 수 있습니다.)

이러한 전략은 복잡하므로 타이머 관리 및 취소에 대한 자세한 내용은 생략하겠습니다. 하지만 Lodash를 사용하면 디바운싱 프로세스가 매우 간단해집니다.

const _ = require('lodash');
const axios = require('axios');

// 예시 API
const fetchDogBreeds = () =>
  axios
    .get('https://dog.ceo/api/breeds/list/all')
    .then((res) => console.log(res.data));

const debouncedFetchDogBreeds = _.debounce(fetchDogBreeds, 1000); // 1초 후
debouncedFetchDogBreeds(); // 일정 시간 후에 데이터 표시

`setTimeout()`을 떠올렸다면, 동일한 작업을 할 수 있지만 더 많은 코드가 필요할 것입니다. Lodash의 디바운스는 강력한 기능을 많이 제공합니다. 예를 들어, 디바운스가 무한정 기다리지 않도록 설정할 수 있습니다. 즉, 함수가 실행될 때마다 키 입력이 있어도 (전체 프로세스가 취소됨) 2초 후에는 API 호출이 반드시 발생하도록 할 수 있습니다. 이를 위해 Lodash `debounce()`에는 `maxWait` 옵션이 있습니다.

const debouncedFetchDogBreeds = _.debounce(fetchDogBreeds, 150, { maxWait: 2000 }); // 250ms 동안 디바운싱, 2초 후 API 요청 전송

더 자세한 내용은 공식 문서를 참고하세요.

배열에서 값 제거

배열에서 항목을 제거하는 코드를 작성하는 것은 귀찮은 일입니다. 항목의 인덱스를 가져오고, 인덱스가 유효한지 확인하고, `splice()` 메서드를 호출해야 합니다. 구문을 매번 찾아봐야 하고, 마지막에는 사소한 버그가 숨어 있을 수 있습니다.

const greetings = ['hello', 'hi', 'hey', 'wave', 'hi'];
_.pull(greetings, 'wave', 'hi');
console.log(greetings);
// [ 'hello', 'hey' ]

다음 두 가지 사항에 주목하세요.

  • 원래 배열이 변경됩니다.
  • `pull()` 메서드는 중복 항목을 포함하여 지정된 모든 항목을 제거합니다.

배열을 두 번째 매개변수로 허용하는 `pullAll()` 메서드를 사용하면 한 번에 여러 항목을 더 쉽게 제거할 수 있습니다. 스프레드 연산자와 함께 `pull()`을 사용할 수 있지만, Lodash는 스프레드 연산자가 언어에 포함되기 전부터 존재했다는 것을 기억하세요!

const greetings2 = ['hello', 'hi', 'hey', 'wave', 'hi'];
_.pullAll(greetings2, ['wave', 'hi']);
console.log(greetings2);
// [ 'hello', 'hey' ]

요소의 마지막 인덱스

JavaScript의 기본 `indexOf()` 메서드는 배열을 스캔할 때 역방향에서 시작해야 하는 경우를 제외하고는 훌륭합니다! 물론, 감소하는 루프를 직접 작성하여 요소를 찾을 수도 있지만, 더 우아한 기술을 사용하는 것이 좋지 않을까요?

다음은 `lastIndexOf()` 메서드를 사용하는 간단한 Lodash 솔루션입니다.

const integers = [2, 4, 1, 6, -1, 10, 3, -1, 7];
const index = _.lastIndexOf(integers, -1);
console.log(index); // 7

하지만 이 메서드는 복잡한 객체를 검색하거나 사용자 정의 검색 함수를 전달할 수 있는 기능이 없습니다.

압축 및 압축 해제

Python을 사용해본 적이 없다면, `zip`/`unzip`은 JavaScript 개발자로서 전체 경력을 통해 알 수 없거나 상상할 수 없는 유틸리티일 것입니다. 그리고 그것은 아마도 타당한 이유가 있을 것입니다. `filter()` 등과 같이 `zip`/`unzip`이 필요한 경우는 드뭅니다. 그러나 이 기능은 잘 알려지지 않은 유틸리티 중 하나이며, 상황에 따라 코드를 간결하게 만드는 데 도움이 될 수 있습니다.

이름과 달리 `zip`/`unzip`은 압축과 관련이 없습니다. 대신 동일한 길이의 배열을 함께 “압축”(`zip()`)하고 “압축 해제”(`unzip()`)하여 동일한 위치의 요소들을 모아 새로운 배열로 만들 수 있는 그룹화 작업입니다. 혼란스러울 수 있으므로, 코드를 살펴보겠습니다.

const animals = ['duck', 'sheep'];
const sizes = ['small', 'large'];
const weight = ['less', 'more'];

const groupedAnimals = _.zip(animals, sizes, weight);
console.log(groupedAnimals);
// [ [ 'duck', 'small', 'less' ], [ 'sheep', 'large', 'more' ] ]

원래 3개의 배열이 2개의 배열만 있는 단일 배열로 변환되었습니다. 각각의 새로운 배열은 동물 종류, 크기, 무게를 나타냅니다. 이제 데이터를 조작하기가 더 쉬워졌습니다. 필요한 모든 작업을 적용한 후 `unzip()`을 사용하여 데이터를 다시 분할하고 원래 형식으로 되돌릴 수 있습니다.

const animalData = _.unzip(groupedAnimals);
console.log(animalData);
// [ [ 'duck', 'sheep' ], [ 'small', 'large' ], [ 'less', 'more' ] ]

`zip`/`unzip` 유틸리티는 매일 사용하는 기능은 아니지만, 필요할 때 유용하게 사용할 수 있습니다.

결론 👨‍🏫

(이 글에서 사용된 모든 코드를 여기에서 직접 테스트해 보세요!)

Lodash 문서에는 놀라운 예제와 함수가 가득합니다. JavaScript 생태계가 복잡해지는 것처럼 보이는 시대에, Lodash는 신선한 공기와 같습니다. 프로젝트에서 이 라이브러리를 적극적으로 사용하는 것을 권장합니다!