본문 바로가기

나의 코드 삽질기

classnames 라이브러리 구현해보기(for 자바스크립트)

반응형

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');
  })
});

 

 

반응형