회사에서 컴포넌트 개발 및 유지보수를 담당하고 있는데 처음부터 다 만들시간이 없어서 기본적으로는 Antd를 커스텀해서 사용하고 있다. 하지만, Atnd를 베이스로 하다보니 커스텀에 제한도 많이 되고 제대로된 개발을 하지 않고 임시방편만을 만드는 것 같아서 직접 간단하게 디자인 시스템을 제작해보기로 했다.
일단 간단한 컴포넌트를 한두개 정도 만들고 피그마 디자인 시스템을 간단하게 만들어서 디자인적인 일관성까지 고려해서 만들어보려고 한다.
가장 먼저 비교적 간단한 컴포넌트인 checkbox를 만들기로 했다. 회사에서는 Antd를 사용하고 평소에 코드도 참고하지만 quasar가 구현한 방향이 조금 더 vuejs 기반 컴포넌트에 적절한 방식이라고 판단해서 quasar의 코드를 참고했다. 근데 참고하다보니 코드가 거의 같아지긴 했는데 일단 잘 만들어진 오픈소스를 이해하는 것도 의미가 있다고 생각해서 그냥 그렇게 진행했다.
체크박스가 체크된 상태와 indeterminate 상태를 어떻게 보여줘야 하는지 몰랐는데 svg를 통해 그려서 해결할 수 있었다.
<div class="r-checkbox__bg">
<svg class="r-checkbox__svg" viewBox="0 0 24 24">
<path
class="r-checkbox__truthy"
fill="none"
d="M4 12.6111L8.92308 17.5L20 6.5" />
<path class="r-checkbox__indet" d="M4,14H20V10H4" />
</svg>
</div>
class 네이밍은 BEM 방법론을 적용했다. r-checkbox__bg는 svg tag를 감싸서 위치를 잡아주고 truthy, indeterminate 두 가지 상태를 보여주기 위해 두 가지 path를 가진다.
path에 대해서도 이번에 처음 알게됐는데 path를 분석해주는 도구랑 함께 보면 이해가 좀 쉽다. 상위 div 클래스에 따라 path를 보여주기 위한 css를 적용하는데 전혀 몰랐던 css들이라 이 부분 이해하는데도 시간이 좀 걸렸다.
export interface Props {
modelValue: any;
value?: any;
trueValue?: any;
falseValue?: any;
indeterminateValue?: any;
...
}
vuejs의 공식문서를 보면 checkbox type의 input은 true-value, false-value를 커스텀하게 사용할 수 있다. 어떤 값이라도 가능하기 때문에 modelValue가 any 타입을 가져야 한다.
const modelIsArray = computed(() => Array.isArray(props.modelValue));
const index = computed(() => {
const value = toRaw(props.value);
return modelIsArray.value === true
? props.modelValue.findIndex((opt: any) => toRaw(opt) === value)
: -1;
});
const isTrue = computed(() =>
modelIsArray.value === true
? index.value > -1
: toRaw(props.modelValue) === toRaw(props.trueValue),
);
const isFalse = computed(() =>
modelIsArray.value === true
? index.value === -1
: toRaw(props.modelValue) === toRaw(props.falseValue),
);
modelIsArray 값을 통해 조건문을 사용한다.isTrue === false && isFalse === false 조건을 통해 판별할 수 있다.const getNextValue = () => {
if (modelIsArray.value === true) {
if (isTrue.value === true) {
const copy = [...props.modelValue] as Array<any>;
copy.splice(index.value, 1);
return copy;
}
return props.modelValue.concat([props.value]);
}
if (isTrue.value === true) {
return props.falseValue;
} else if (isFalse.value === true) {
return props.trueValue;
}
return getIndetNextValue();
};
const getIndetNextValue = () => {
return props.trueValue;
};
const onKeyup = (e: KeyboardEvent) => {
if (e.code === 'Enter' || e.code === 'Space') {
onClick(e);
}
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault();
e.stopPropagation();
}
};
const tabIndex = computed(() =>
props.disabled === true ? -1 : props.tabIndex,
);
const attributes = computed(() => {
const attrs: HTMLAttributes = {
tabindex: tabIndex.value,
role: 'checkbox',
'aria-label': props.label,
'aria-checked':
isIndeterminate.value === true
? 'mixed'
: isTrue.value === true
? 'true'
: 'false',
'aria-disabled': props.disabled === true ? 'true' : 'false',
};
return attrs;
});
const formAttrs = computed(() => {
const attrs = {
type: 'checkbox',
// dom property
'.checked': isTrue.value,
// dom attribute
'^checked': isTrue.value === true ? true : void 0,
name: props.name,
value: modelIsArray.value === true ? props.value : props.trueValue,
};
return attrs;
});
금방 만들 줄 알았는데 생각보다 쉽지 않았다. qasar 코드를 참고하면서 느꼈지만 생각보다 고려할게 많고 코드의 가독성도 중요하기 때문에 최대한 의미있는 함수 단위로 묶으려고 했다.
일단 필요한 기능은 다 정의가 되었고 이후에 form, 디자인 시스템 적용할 때 다시 작업할 예정
전체 코드는 아래 링크에서 확인 가능하니다.
https://github.com/Jaejin-Song/Design-System