카테고리 없음

[오픈소스코드리뷰] classNames

유지남 2021. 1. 25. 21:44

들어가며

문자열로만 처리해야 하는 class name을 조건부로 결합하기 위한 간단한 Javascript 유틸리티입니다.
하지만 classnames project는 2년간 업데이트되지 않고 있으니, 사용법이 같은 clsx 도 같이 살펴봐 두시면 좋습니다.

classnames는 동적 스타일에 주로 사용되며, react project에서도 굉장히 많이 사용되고 있습니다.
tailwind, bulma, bootstrap과 같은 css framework를 사용하고 계신다면 더할 나위 없는 좋은 유틸리티입니다.

코드가 길지 않으니 빠르게 훑어보겠습니다.
현재 리뷰 버전은 2.2.6입니다.

리뷰

// packages.json
// 생략 ...
"scripts": {
  "benchmarks": "node ./benchmarks/run",
  "benchmarks-browserify": "./node_modules/.bin/browserify ./benchmarks/runInBrowser.js >./benchmarks/runInBrowser.bundle.js",
  "benchmarks-in-browser": "./node_modules/.bin/opn ./benchmarks/benchmarks.html",
  "test": "mocha tests/*.js"
},
// 생략 ...
"devDependencies": {
  "benchmark": "2.1.4",
  "browserify": "16.2.3",
  "mocha": "5.2.0",
  "opn-cli": "4.0.0"
}
// 생략 ...


특별한 script 는 없습니다. 단일 테스트와 퍼포먼스 테스트를 위해 benchmarks, mocha를 사용한다 정도만 정의되어 있고,
다음으로 의존성 내용 중에 opn-cli 는 open-cli로 이름이 변경되었습니다.
opne-cli는 터미널에서 지정한 파일을 브라우저에 볼 수 있게 해 주어 Cross browser 체크시 유용하게 사용되는 유틸리티입니다.
Webpack 대신 browserify를 이용하는 것 같은데, scripts 에 명시되지 않은 거 보니,
사용하지는 않는 것 같습니다.

// 생략 ...
bind.js
bind.d.ts
index.js
index.d.ts
// 생략 ...

타입 스크립트 환경에서도 사용 할 수 있도록 *. d.ts 파일이 있고,
안에 내용을 살펴보면 array, object를 포함하여 문자열, 숫자 등을 받도록 되어 있습니다.

// index.js
(function () {
  'use strict'; // 1
  var hasOwn = {}.hasOwnProperty; // 2
  //  생략 ...

index.js 는 전체 58 라인으로 매우 짧은 코드입니다.
위에서부터 살펴보면

  1. use strict는 엄격 모드로 이는 sloppy mode(느슨한 모드)에서 묶인 되었던 에러들을 표시해 줌으로써 엄격하게 문법 검사를 하기 위한 선언문입니다.
    근래에 많이 사용하고 있는 babel 같은 transcompiler 에도 이런 선언문이 포함되어 있습니다.
  2. 자주 사용하는 함수를 쉽게 사용하기 위해 짧은 변수로 정의해 두었습니다.

    실무에서도 hasOwnProperty, addEventListener, getElementsByClassName과 같은 긴 함수명을 짧게 정의하여 사용하기도 합니다.
// index.js
// 생략 ...
function classNames() {
var classes = [];

for (var i = 0; i < arguments.length; i++) {
  var arg = arguments[i];
  if (!arg) continue;

  var argType = typeof arg;
  // 생략 ...
}
// 생략 ...

arguments는 classNames 함수로 전달된 모든 매개변수를 배열 형태로 사용할 수 있고,
각 매개변수 타입을 체크하여, classes 배열에 담아주는 반복문입니다.
여기서 string, number와 같은 단순 타입 형태는 별 다른 처리 없이 classes 배열에 담고,

// index.js
// 생략 ...
} else if (Array.isArray(arg)) {
  if(arg.length) {
    var inner = classNames.apply(null, arg);
    if (inner) {
      classes.push(inner);
    }
  }
}
// 생략 ...

array 인 경우 재귀 호출로 처리하고 있습니다.
여기서 apply는 배열의 내용을 매개 변수로 넘겨주는 함수입니다.
예를 들어
function author(name, age){}와 같은 함수가 있다고 가정할 때,
author.apply(null, ['유지남', '18']) 형태로 호출할 수 있습니다.
이 밖에 bind와 call 도 많이 사용하니 꼭 예제를 통해 익혀 두시기를 바랍니다.

// index.js
// 생략 ...
} else if (argType === 'object') {
  if (arg.toString !== Object.prototype.toString) {
      classes.push(arg.toString());
  } else {
    for (var key in arg) {
      if (hasOwn.call(arg, key) && arg[key]) {
        classes.push(key);
      }
    }
  }
}
// 생략 ...

위에서 선언해둔 hasOwn을 여기서 사용하네요.
for.. in 문으로 object 내에 key를 가져와서, 참인 값만 classes 배열에 넣어 주고 있습니다.
이 부분을 es6+ 로 사용한다면

// A: 함수형을 좋아한다면...
const arg = { a: 1, b: 0, c: false };
const res = Object.entries(arg)
	.filter(([keys, value]) => !!value)
	.map(([key]) => key);
classes = classes.concat(res);
// B: 미묘한 퍼포먼스라도 중하다면...
const res = Object.entries(arg).reduce(
	(a, [key, value]) => (!!value ? a.concat([key]) : a),
	[]
);
classes = classes.concat(res);

이렇게 할 수도 있을 것 같습니다.

퍼포먼스 차이보다 스타일의 차이라고 보시면 될 것 같습니다.

// index.js
// 생략 ...
  return classes.join(' ');
}

배열에 담긴 내용을 문자열로 변환하여 반환함으로써 함수가 끝납니다.

마치며

나머지 내용은 나중에 commonjs, amd에 관한 주제가 있다면 그때 설명하겠습니다.

그럼 이번 시간에 다룬 arguments, apply, Object entries의 사용법을 짧게 알아보았습니다.
모두 자주 사용하는 것들이니 다시 한번 코드를 살펴보시고, 꼭 익혀 두시길 바랍니다.

감사합니다.

https://github.com/JedWatson/classnames

JedWatson/classnames

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

github.com