티스토리 뷰

1. clsx 분석

clsx의 테스트 코드를 보면 다형성이 뛰어나다. 

다양한 입력을 적절히 처리하고 있음을 알 수 있다.
전부는 아니지만 몇 가지만 확인해 보자면

// 가변 인자중에 truty한 값으로 클래스명을 만듦
test("(compat) joins arrays of class names and ignore falsy values", () => {
	const out = clsx("a", 0, null, undefined, true, 1, "b");
	assert.is(out, "a 1 b");
});

// 리터럴객체가 입력되면 값이 truty한 key로 클래스명을 만듦
test("(compat) keeps object keys with truthy values", () => {
	const out = clsx({ a: true, b: false, c: 0, d: null, e: undefined, f: 1 });
	assert.is(out, "a f");
});

// 배열도 적절히 처리
test("(compat) joins array arguments with string arguments", () => {
	assert.is(clsx(["a", "b"], "c"), "a b c");
	assert.is(clsx("c", ["a", "b"]), "c a b");
});

// 중첩배열, 중첩배열 내의 객체도 적절히 처리
test("(compat) handles deep array recursion", () => {
	assert.is(clsx(["a", ["b", ["c", { d: true }]]]), "a b c d");
});

다형성이 매우 좋다! 구현부를 살펴보자

https://github.com/lukeed/clsx/blob/master/src/index.js

 

clsx/src/index.js at master · lukeed/clsx

A tiny (239B) utility for constructing `className` strings conditionally. - lukeed/clsx

github.com

빈 줄을 모두 포함해도 43줄 밖에 안된다. 단순한 기능이긴 하지만 어떻게 이런 다형성을 구현했을까?

가장 먼저 clsx는 가변인자를 arguments로 받아서 for문으로 순회하며 truty 한 값인지 체크한다.

for (; i < len; i++) {
    if ((tmp = arguments[i])) {
        if ((x = toVal(tmp))) {
            ...문자열 연결
        }
    }
}

여기서 clsx(0, false, "") 이런 값들이 제외된다.

중첩 if문에서 toVal함수를 호출하여 각 인자들이 truty 한 지 다시 한번 체크한다.
toVal은 무슨 함수일까?

소스코드를 보면 string, number타입의 값을 문자열로 변환한다. 나중에 classnames라이브러리와 비교하겠지만, number타입도 문자열로 합쳐준다는 차이점이 있다. 쓸모가 있을까..?

 

그리고 typeof 연산자로 object타입이 나오는 케이스에 대한 처리를 한다. 

배열인 경우 재귀적으로 순회하면서 중첩배열의 모든 truty한 값을 문자열로 연결한다.

else문에서 배열이 아닌 경우를 처리하는데, for in문으로 작성되어 있으므로 Map, Set객체는 순회하지 않고 객체를 처리한다.

각 객체의 value값이 truty한 경우 key값을 문자열로 연결한다. 

 

전체적으로 보면 if문으로 각 인자의 타입에 따라 적절한 방법으로 순회하며 truty 한 값을 걸러내고 있음을 알 수 있다.

 

이러한 기능 덕분에 tailwindcss같은 라이브러리와 함께 사용하여 조건부 스타일링을 구현하기 쉽게 한다.

<div
  className={clsx(
    'alert',
    {
      'alert-info': variant === 'info',
      'alert-success': variant === 'success',
      'alert-error': variant === 'error'
    },
    className // 외부에서 전달된 클래스도 병합
  )}
>
  {children}
</div>

<div
  className={clsx(
    'rounded-lg p-4 shadow',
    isActive 
      ? 'bg-blue-500 text-white'
      : 'bg-white text-gray-800'
  )}
>
  Content
</div>

등등

 

2. classnames 분석

그럼 classnames와 비교해보자.

테스트 코드를 보면 clsx와 거의 동일하다! classnames가 먼저 등장했으니 clsx를 구현할 때 classnames의 기존 기능을 포함하려고 했나 보다. 약간의 차이점이 있는데, classnames에는 toString 메서드를 가진 object에 대한 케이스가 추가로 존재한다. 

https://github.com/JedWatson/classnames/blob/main/tests/index.js

 

classnames/tests/index.js at main · JedWatson/classnames

A simple javascript utility for conditionally joining classNames together - JedWatson/classnames

github.com

거의 마지막에 보면 

handles toString() method defined on object, handles toString() method defined inherited in object

두 가지 테스트 케이스가 있다.

왜 이런 케이스까지 검증했을까 궁금하여 찾아보니 
https://github.com/JedWatson/classnames/issues/153

 

Classnames ignore toString() method · Issue #153 · JedWatson/classnames

I found very useful to have an ability to pass objects with own toString() method to classNames function. ClassNames makes checking is argument type a string, but doesn't check that it is an object...

github.com

리액트 클래스형 컴포넌트를 사용하던 때에 클래스에 toString() 메서드로 클래스명을 입력하여 사용할 필요가 있었던 것 같다. 

아직 정확히 클래스형 컴포넌트에 toString() 메서드가 어떤 목적으로 쓰였는지는 잘 모르겠다. 하지만 분명한 건 classnames라이브러리에는 이 케이스에 대응하기 위한 코드가 있었다.
https://github.com/JedWatson/classnames/blob/main/index.js

 

classnames/index.js at main · JedWatson/classnames

A simple javascript utility for conditionally joining classNames together - JedWatson/classnames

github.com

개인적으로 소스코드를 좀 더 읽기가 쉬웠다.

export default로 내보내지는 classNames함수도 마찬가지로 가변인자를 순회하면서 truty 한 값에 대해 처리를 한다.

appendClass함수는 

function appendClass(value, newClass) {
    if (!newClass) {
        return value;
    }

    return value ? value + " " + newClass : newClass;
}

첫 번째 인자와 두 번째 인자를 문자열로 결합하는 함수다.

그리고 두 번째 매개변수를 입력할 때

parseValue(arg)

parseValue함수를 호출하여 가변인자를 적용하는데, 이 함수가 clsx의 toVal 같은 함수이다.

function parseValue(arg) {
	if (typeof arg === "string") {
		// string일때 처리
	}

	if (typeof arg !== "object") {
		// string 이 아닌데 object도 아니면 빈문자열 반환하며 얼리리턴
		// number에 대한 처리는 없다.
	}

	if (Array.isArray(arg)) {
		// typeof object가 배열인 경우 재귀호출로 truty한 값을 연결
	}

	if (
		arg.toString !== Object.prototype.toString &&
		!arg.toString.toString().includes("[native code]")
	) {
		// toString을 직접 구현한 경우에 대한 처리
		// toString 메서드 호출값을 반환한다.
	}

	let classes = "";

	for (const key in arg) {
		if (hasOwn.call(arg, key) && arg[key]) {
			// 객체인 경우에 대한 처리
            // clsx와 마찬가지로 value가 truty한 경우 key를 문자열에 연결해서 반환한다.
		}
	}

	return classes;
}

 

객체인 경우를 처리하는 부분에 객체 프로토타입에 hasOwnProperty메서드를 호출해서 검증하는 부분이 있는데,

이 부분도 마찬가지로 리액트 클래스형 컴포넌트를 사용하는 코드에 대한 처리라고 볼 수 있다.

리액트 클래스 컴포넌트는 React.Component 클래스를 상속하여 구현하는데, classNames 함수에 클래스형 컴포넌트를 넣었을 때 React.Component클래스에 있는 수많은 메서드가 모두 클래스명으로 입력되는 것을 방지할 수 있다.

(아. 그럼 toString()을 직접 구현하는 이유도 컴포넌트의 클래스명을 명시적으로 지정하고 싶어서인가..?)

 

3. 정리

clsx와 classnames 라이브러리 비교결과 리액트 최신버전을 사용하고 함수형 컴포넌트로 작성할 것이라면 clsx가 더 나은 것 같다.

classnames는 클래스형 컴포넌트에 대한 처리를 하기 위해 객체를 인자로 하는 경우 함수호출마다 Object.prototype의 hasOwnProperty를 호출하기 때문에 함수형 컴포넌트 사용 시 불필요한 메서드 호출이 많다고 볼 수 있다. 

게다가 clsx에서는 lite 한 코드도 제공하는데, string타입에 대해서만 기능을 지원한다. 배열이나 객체를 사용하지 않는다면 훨씬 단순한 코드를 제공한다. 단순히 클래스명을 조건부로 이어 붙일 때는 이게 나을 것 같다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함