본문 바로가기
  • David is trying his best.
JS

find() - Lodash 유틸함수 직접 구현 | TypeScript

by myDav 2024. 6. 9.

 

복잡한 형태를 처리할 수 있는 Lodash 의 find를 구현하기 전에, 가장 기본적인 형태인 JS의 find를 구현해본 후, Lodash 가 제공하는 find 함수를 만들어보자.

 

find를 구현할 때, 문제 정의가 가장 중요하다.

 

JS 기본 'find' 함수 구현 -  Array.prototype.find()

→ 배열을 받아 넘겨주는 콜백 함수로 검사하는 find를 만들어보자.

 

구현 순서

  1. 배열 순회: 배열을 순회하며 각 요소를 조건과 비교한다.
  2. 조건 만족 여부 확인: 요소가 조건을 만족하면 첫번째 해당 요소를 반환한다.
  3. 조건을 만족하는 요소가 없으면 undefined를 반환한다.

구현 코드

function find<T>(collection: T[], predicate: (el) => boolean): T | undefined {
    // 배열 순회
    for (let element of collection) {
    	// 조건 만족 여부 확인
    	if (predicate(element)) {
        	return element;
        }
    }
    // 조건을 만족하는 요소가 없으면 undefined 반환 
    return undefined;
}

 

Line 1 설명

- collection은 제레릭 타입 <T> 를 사용해 어떤 타입의 배열이든 처리할 수 있도록 한다. 

- predicate 는 T 타입의 요소를 받아서 boolean 값을 반환하는 함수라고 타입을 지정한다.

- 조건을 만족하는 첫번째 T 를 반환, 또는 조건을 만족하는 요소가 없다면 undefined 를 반환한다.

 

이를 기본으로 삼고, 이제 좀 더 복잡한 Lodash 의 find 함수를 직접 구현해보자. 

 

Lodash 가 제공하는 find 함수

Lodash 의 find 함수는 콜백 함수 뿐 아니라 객체, 배열, 문자열 등의 더 많은 옵션을 가진 검색 기능을 제공하고, 배열 뿐 아니라 객체에서도 사용할 수 있기 때문에 추가적인 로직이 필요하다.

 

이를 위해 find 함수를 설계할 때의 구상과정은 문제를 해결하기 위해 필요한 여러 단계를 논리적으로 나누고, 이를 개별 함수로 분리하여 각 단계를 구현하는 방식으로 이루어진다. 이 과정은 다음과 같다.

 

    1. 문제 정의: 먼저 find 함수의 요구사항을 명확히 정의한다. 

배열이나 객체를 순회하며 주어진 조건을 만족하는 첫번째 요소를 찾는다.

→ 이 주어진 조건은 다양한 형태, 즉 함수, 객체, 배열, 문자열 등을 가질 수 있다.

 

 

    2. 문제 분해: 위의 복잡한 문제를 더 작은 하위 문제로 분해한다. 위에서 밑줄 친 주요한 두 문제를 하위 문제로써 풀어야 한다.

→ 컬렉션을 순회하는 로직

→ 주어진 조건(predicate)를 평가하는 로직

 

 

    3. 컬렉션을 순회하는 로직을 먼저 알아보자.

→ collection이 배열이라면 for...of 로 순회, object 라면 for...in 로 순회하기로 한다.

→ 이를 사용하여 find 함수의 큰 틀을 잡아 놓는다.

function find<T>(collection: T[] | { [key: string]: T }, predicate: (item: T) => boolean) {
    // 배열, 또는 객체 순회
    if (Array.isArray(collection) {
    	for (const element of collection) {
        // TODO: 조건 비교
        // do something...
        return element
        }
    } else {
    	for (const key in collection) {
        // TODO: 조건 비교
        // do something...
        return collection[key];
        }
    }
    return undefined;
}

 

   

    4. Predicate 평가하는 로직을 다음으로 알아보자.  

→ Predicate는 다양한 형태를 가질 수 있기 때문에 이를 일관된 방식으로 처리해야 한다. 그리고 JS find에서 봤듯이 콜백 함수를 받았을 때처럼, 개별 아이템을 받았을 때 True/False 를 리턴해주는 함수를 만들어줘야 한다. 이를 위해 'iteratee'라는 함수를 만들고 아래의 다양한 형태의 조건을 처리하도록 하자.

  • 함수 형태 → 콜백 함수, 그대로 반환해서 사용
  • 객체 형태 → 객체의 key를 뽑아 순회하면서 값을 비교하는 함수 필요
  • 배열(Tuple) 형태 → 객체가 들어온 것이라 생각하고, Tuple의 키, 값을 뽑아 객체의 키, 값과 비교한다.
    ▶ 배열은 Tuple 형태만 생각하자.
  • 문자열 형태 → 객체가 들어온 것이라 생각하고, 'item[문자열]' 을 검사하자.
    ▶ collection의 요소가 객체가 아닌 단순 문자열이라면 'item === 문자열' 로 비교한다.
type Predicate<T> = ((item: T) => boolean) | Partial<T> | [keyof T, unknown] | keyof T;

function iteratee(predicate: Predicate): (item: T) => boolean {
    if (typeof item === 'function') { // 함수일때
    	return predicate as (item: T) => boolean;
    } else if (type of item === 'object' && !Array.isArray(predicate)) { // 객체일때
    	return (item: T) => {for (const key in predicate {...do something}};
    } else if (Array.isArray(predicate) && predicate.length === 2) { // 배열일때
    	const [key, value] = predicate;
    	return (item: T) => item[key] === value;
    } else if (typeof predicate === 'string') { // string 타입일때
    	return (item: T) => Boolean(item[predicate as keyof T]);
    }
    return (_: T) => false;
}

 

+) 왜 함수 이름이 'iteratee' 인가요? 'iterate' 의 오타인가요?

더보기

'iteratee' 라는 이름은 Lodash 에서 사용되는 개념에서 비롯된 것이다. Lodash 'iteratee' 는 함수를 생성하거나 반환하는 함수로, 특정 조건이나 기준에 따라 동작하는 함수를 만드는 역할을 한다. Lodash에서는 '_.iteratee' 를 사용하여 다양한 형식의 인자를 기준을 함수를 생성한다.

 

여기서 iteratee 함수는 find 에서 사용할 조건 평가 함수를 생성하기 때문에, 이와 같은 이름을 사용하는 것이 일반적이다. 반면, 'iterate'는 단순히 컬렉션을 순회(iterate)하는 작업을 의미하기 때문에, 이 함수의 역할과는 다르다고 할 수 있다.

 

그러나 혼란을 줄이기 위해 함수 이름을 변경할 수도 있다. 예를 들어, 'createPredicateFunction' 처럼 더 명확하게 함수의 역할을 설명하는 이름을 사용할 수 있다.

 

    4-1. isMatch 함수

→ 객체가 predicate로 들어올 경우, 다시 predicate 객체의 순회가 필요할테니, 객체를 순회하면서 속성들이 일치하는지 비교하는 isMatch 함수로 나누면 좋을 것 같다. iteratee 함수가 복잡해지는 것도 방지할 수 있다. 

function isMatch<T>(item: T, predicate: Partial<T>) {
    // predicate 가 객체 형태일 때, 객체 순회
    for (const key in predicate) {
    	// 같은 요소가 없으면 바로 false 리턴
    	if (item[key] !== predicate[key] {
        	return false;
        }
        // 아니면 계속 순회하면서 마지막에 true 리턴
    }
    return true;
}

 

    4-2. iteratee() 완성

type Predicate<T> = ((item: T) => boolean) | Partial<T> | [keyof T, unknown] | keyof T;

function iteratee<T>(predicate: Predicate<T>) {
    if (typeof predicate === 'function') {
    	return predicate as (item: T) => boolean;
    else if (typeof predicate === 'object' && !Array.isArray(predicate)) {
    	return (item: T) => isMatch(item, prediate);
    else if (Array.isArray(predicate)) {
    	const [key, value] = predicate; // Tuple
        return (item: T) => item[key] === value;
    else if (typeof predicate === 'string') {
    	return (item: T) => Boolean(item[predicate]);
    }
    return (_: T) => false;   
}

 

   

    5. 최종 함수 구현

type Predicate<T> = ((item: T) => boolean) | Partial<T> | [keyof T, unknown] | keyof T;

function find<T>(collection: T[] | { [key: string]: T }, predicate: Predicate<T>) {
    const predicateFunction = iteratee(predicate);
    
    if (Array.isArray(collection)) {
    	for (const element of collection) {
        	if (predicateFunction(element)) {
            	return element;
            }
        }
    } else {
    	for (const key in collection) {
        	if (predicateFunction(collection[key])) {
            	reurn collection[key];
            }
    }
    return undefined;
}

function iteratee(predicate: Predicate<T>) {
    if (typeof predicate === 'function') {
    	return predicate as (item: T) => boolean;
    else if (typeof predicate === 'object' && !Array.isArray(predicate)) {
    	return (item: T) => isMatch(item, predicate);
    else if (Array.isArray(predicate) && predicate.length === 2) {
    	// let. predicate = Tuple
    	const [key, value] = predicate;
    	return (item: T) => item[key] === value;
    } else if (typeof predicate === 'string') {
    	return (item: T) => Boolean(item[predicate]);
    }
    return (_: T) => false;
}

// typeof predcate === 'object' 
function isMatch<T>(item: T, predicate: Partial<T>) {
    for (const key in predicate) {
    	if (item[key] !== predicate[key] {
        	return false;
        }
    }
    return true;
}

 


정리

TypeScript 라서 함수가 거창해 보이지만, 사실 JS로 별거 없다.

function find(collection, predicate) {
    const predicateFunction = iteratee(predicate);
    
    if (Array.isArray(collection) {
    	for (const element of collection) {
        	if (predicateFunction(collection)) {
        		return element;
            }
        }
    else {
    	for (const key in collection) {
        	if (predicateFunction(collection[key]) {
            	return collection[key];
            }
        }
    }
    return undefined;
}

function iteratee(predicate) {
    if (typeof predicate === 'function') {
    	return predicate;
    else if (typeof predicate === 'object' && !Array.isArray(predicate)) {
    	return item => isMatch(item, predicate);
    else if (Array.isArray(predicate)) {
    	const [key, value] = predicate;
        return item => item[key] === value;
    else if (typeof predicate === 'string') {
    	return item => Boolean(item[predicate]);
    }
    return () => false;
}

function isMatch(item, predicate) {
    for (const key in predicate) {
    	if (item[key] !== predicate[key]) {
        	return false;
        }
    }
    return true;
}

 

  1. collection을 받아 Array/Object 형태에 맞게 순회를 하고,
  2. predicate 형태에 따라 그에 맞는 boolean을 반환하는 함수를 만들어준다.

find 함수를 만들 때 위의 가장 주요한 문제를 위와 같이 나눠 볼 수 있을 것 같다.

 

사실 어떻게 구현하는지도 중요하지만, 문제를 어떻게 정의하고, 어떻게 논리적으로 풀어나갈 것인지가 더 중요한 것 같다.

 

어려웠던 점은 다양한 형태의 predicate를 어떻게 처리하느냐였고, 이 처리를 특정 기준에 따라 함수를 리턴하는 함수로 만들어서 해결하는 개념(iteratee)과 과정이었다. 

 

+)

TS에서 타입 정의 시,

type Predicate<T> = (item: T) => boolean | Partial<T> | [keyof T, unknown] | keyof T;

type Predicate<T> = ((item: T) => boolean) | Partial<T> | [keyof T, unknown] | keyof T;

이 둘은 엄연히 다른 것이다.

첫 번째는 함수가 리턴하는 종류가 4가지가 있다는 것이고,

두 번째는 Predicate 의 타입이 4가지가 있다는 것이다.


Reference

Array.prototype.find() -  https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find

 

find() Lodash - https://lodash.com/docs/4.17.15#find

 

'JS' 카테고리의 다른 글

CJS vs ESM  (0) 2024.06.02
요즘은 왜 jQuery 를 안쓰는가 ?  (0) 2024.05.14
변수 - JS Deep Dive 와 ECMAScript 참고  (0) 2024.05.06