classnames 라이브러리는 React에서 스타일을 사용하신 분이라면 알고 있을 겁니다.
React에서 className을 활용해서 스타일을 다음과 같이 지정해 줍니다.
<select
className={style.select}
value={group}
onChange={(e) => setGroup(e.target.value)}
>
{groups.map((value) => {
return <option value={value}>{value}</option>;
})}
</select>
위와 같이 설정하면 다음과 같이 React 환경 내에서 클래스를 암호화해줍니다.

다중 클래스를 적용하기 위한 방법은 여러 가지가 있습니다.
템플릿 리터럴 방식으로 클래스 이름을 넘겨줄 수도 있고, 배열의 형태로 넘긴 후 문자열로 합칠 수도 있지만 가독성이 많이 떨어집니다.
이를 개선하기 위해서 나온 방식이 classnames입니다.
전체 코드는 다음과 같습니다.
/*
classnames 함수
파라미터: classNames(여러개의 클래스 이름)
null, undefined, '', 숫자, 배열, 객체 등 모두 허용
*/
const classnames = (...classNames) => {
const newArray = [];
for (let i = 0; i < classNames.length; i++) {
const current = classNames[i];
if (current === null || current === undefined || current === '') continue;
if (Array.isArray(current)) {
for(let i = 0; i < current.length; i++) {
newArray.push(current[i]);
}
continue;
}
if (typeof current === 'object') {
for (const key in current) {
if (current[key]) {
newArray.push(key);
}
}
continue;
}
if (typeof current === 'string') {
const trimmed = current.trim();
if (trimmed) {
newArray.push(trimmed);
}
continue;
}
if (typeof current === 'number') continue;
}
return newArray.join(' ');
};
export default classnames;
classnames 라이브러리를 구현하기 위해서 저는 다음의 조건을 생각했습니다.
1. 빈 문자열이 있는 경우
2. null이 있는 경우
3. undefined가 있는 경우
4. 숫자가 있는 경우
5. 객체로 넘어오는 경우 ({'text': true}
6. 배열로 넘어오는 경우
7. 문자열로 넘어오는 경우
* 1~3의 케이스는 다음의 코드로 값을 판단하여 예외 처리해 주었습니다.
if (current === null || current === undefined || current === '') continue;
* 숫자가 있는 경우는 값으로 판단하는 것보단 type으로 판단하는 것이 좋을 것 같아서 typeof로 판단하여 예외 처리해 주었습니다.
if (typeof current === 'number') continue;
* 객체로 넘어오는 경우는 typeof로 판단한 후에 key 값에 해당되는 값이 있는 것만 넣어주었습니다.
if (typeof current === 'object') {
for (const key in current) {
if (current[key]) {
newArray.push(key);
}
}
continue;
}
* 배열로 넘어오는 경우는 Array.isArray로 배열인지 확인 후에 값을 넣어주었습니다.
if (Array.isArray(current)) {
for(let i = 0; i < current.length; i++) {
newArray.push(current[i]);
}
continue;
}
* 문자열로 넘어오는 경우는 공백을 고려해야 하므로 공백을 제거한 후에 넣어주었습니다.
if (typeof current === 'string') {
const trimmed = current.trim();
if (trimmed) {
newArray.push(trimmed);
}
continue;
}
테스트 코드는 다음과 같습니다.
import classnames from '../classnames';
describe('classnames', () => {
test('should combine multiple string arguments', () => {
expect(classnames('class1', 'class2', 'class3')).toBe('class1 class2 class3');
});
test('should handle empty strings', () => {
expect(classnames('class1', '', 'class2')).toBe('class1 class2');
});
test('should handle null values', () => {
expect(classnames('class1', null, 'class2')).toBe('class1 class2');
});
test('should handle undefined values', () => {
expect(classnames('class1', undefined, 'class2')).toBe('class1 class2');
});
test('should handle numbers', () => {
expect(classnames('class1', 123, 'class2')).toBe('class1 class2');
});
test('should handle mixed types', () => {
expect(classnames('class1', null, undefined, '', 123, 'class2')).toBe('class1 class2');
});
test('should return empty string for no valid arguments', () => {
expect(classnames(null, undefined, '', 123)).toBe('');
});
test('should handle single class', () => {
expect(classnames('class1')).toBe('class1');
});
test('contain Object', () => {
expect(classnames({text: true, 'text-1': true})).toBe('text text-1');
});
test('contain Array', () => {
expect(classnames(['text', 'text-1'])).toBe('text text-1');
})
});
