동아리 스터디 중 DropdownList 컴포넌트를 가용성 있게 만들기로 했다.
컴포넌트의 가용성?
Typescript를 공부하면서 범용성 좋은 컴포넌트를 만들어보진 못한 것 같아, 새로운 Dropdown 컴포넌트를 구성하기 시작했다.
type SingleItem = string | number; //string이거나 number인 경우
interface ObjectItem {
id: string | number;
name: string | number;
label: string | number;
value: SingleItem;
}
각 사용 범위에 맞게 item과 value를 정해주고, Dropdown의 Props로 number, string, object 객체든 뭐든 받아낼 준비를 해야 했다.
하지만, Typescript라는 특성으로 인해서, 각 아이템의 명확한 타입을 지정해주지 않으면, 완성도 높은 컴포넌트를 작성할 수 없다는 것을 깨닫는 것은 별로 오래 걸리지 않았다;
export type DropdownItem = SingleItem | ObjectItem;
export type을 통해 다른 위치에서도 Dropdown에 넣을 수 있는 Item의 타입을 선언했다.
interface Props {
items: SingleItem[] | ObjectItem[];
selected: SingleItem | ObjectItem;
}
위와 같은 형식으로 코드를 진행함에 있어서 사실 큰 문제는 없었다.
대충 뚜뚜따따 해서 만든 디자인에 대강 내용을 때려넣고 useState를 남용하면서 만들면 누구나 쉽게 Dropdown리스트는 만들 수 있다.
const Dropdown = ({ items, selected }: Props) => {
const [isOpen, setIsOpen] = useState('closed');
const [label, setLabel] = useState('드롭다운 테스트');
const [icon, setIcon] = useState('▼');
const onClickDrop = useCallback(() => {
if (isOpen === 'closed') {
setIsOpen('open');
setIcon('▲');
} else {
setIsOpen('closed');
setIcon('▼');
}
}, []);
const onClose = useCallback(() => {
setIsOpen('closed');
setIcon('▼');
}, []);
const ref = useOutsideClick(onClose);
const onChangeLabel = (text: string | number) => {
let temp = RefineItemType(text);
setLabel(temp);
console.log(text);
};
return (
<div className={styles.container}>
<ul className={styles.itemContainer} ref={ref}>
<button type="button" className={styles.label} onClick={onClickDrop}>
<text>{label}</text>
<text>{icon}</text>
</button>
{items.map((item, index) => (
<li key={`${item}_${index}`} className={cs(styles.itemWrapper, styles[isOpen])} onClick={() => onChangeLabel(items[index])}>
{items[index]}
</li>
))}
</ul>
<div>{label}</div>
</div>
);
};
export default memo(Dropdown);
사실 만드는 과정이 어려운 건 아니지만, 중요한 것은 ‘범용성’
범용성 있는 컴포넌트 형성을 위해 타입과 관련된 구글링을 진행하다가 제대로 사용할 줄은 몰랐던 ‘Generic’에 대해서 보게 되었다.
TypeScript Docs 에서도 잘 정의되고, 일관된 API, 재사용성이 높은 컴포넌트를 강조하고 있다.
⇒ 소프트웨어 엔지니어링에서 매우 중요한 부분이다!
C#와 Java와 같은 기본 언어에서 재사용성을 높이는 도구는 ‘Generics’라고 설명하고 있다.
예제를 살펴보자
function identity(arg: number): number {
return arg;
}
들어오는 인자를 그대로 반환하는 함수가 있다고 할 때, 우리는 인자에 타입을 지정하여, 특정 타입임을 정의해야 한다.
(혹은 변수 : any
와 같은 형식으로 기술할 수 있겠다)
행여나 우리가 any라는 타입으로 기술하였다고 한들 문제가 되진 않는다. 다만, 어떤 타입을 넘겨도 any 타입이 반환된다는 정보만 있어, 문제가 생기는 것이다.
function identity<Type>(arg: Type): Type {
return arg;
}
대신 무엇이 반환되는지 확인할 수 있도록 Type이라는 변수를 추가해보자.
⇒ identity 라는 함수에 Type
이라는 타입 변수
를 추가하고, 이를 유저가 준 인수의 타입을 캡처한다. (number
나 string
처럼)
즉, 받는 인자와 반환하는 인자를 같은 타입을 사용할 수 있도록 통일한다.
제네릭을 사용하면, 타입을 불문하고 정보 손실 없이 동작할 수 있도록 정확한 기능을 제공한다.
function identity<Type>(arg: Type): Type {
return arg;
}
//identity 라는 함수에 <Type>이라는 타입 변수 추가
function identity<Type>(arg: Type): Type {
return arg;
}
//arg는 Type 변수에 명시된 타입을 받아서 Type을 반환한다.
우리는 결국 범용성 있는 코드를 작성하기 위해 제네릭을 사용한다는 것을 배웠다.
그렇다면, 함수에 Props가 필요한 상황이라고 가정해보자.
interface
혹은 type
을 지정하여 사용하게 될 것이다.
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
위 처럼 작성된 예제가 있다고 가정하자.
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
아주 조금 수정이 이뤄졌다.
그럼에도, GenericIdentityFn
함수를 사용할 때, 타입인수를 명확하게 작성해주는 것은 타입의 제네릭을 설명하는데 큰 도움을 준다.
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length); //Property 'length' does not exist on type 'Type'.
return arg;
}
위처럼 작성하면, length에는 오류가 생긴다.
“Type에 들어오는 변수 중에서는 length가 없는 애들도 있을 수 있어!”
무작정 타입을 설정해주는 것도 물론 좋은 방지책이 될 수는 있지만, 꾸준히 말했던 범용성 적인 측면에서 보았을 때,
모두 충족시켜주는 코드를 작성하는 것도 큰 도움이 될 수 있다.
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
여기서 말하는
length
는Number
야! 그러니까 당황하지 마렴 ^,^
interface
를 통해 작성해주고, Type
의 extends
를 통해 제약사항을 명시하게 되면, 문제는 사라진다.
loggingIdentity(3);
//Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
그렇게 되면, 세부적인 오류까지 잡아낼 수 있다는 장점이 있고, 들어올 수 없는 인자들까지 확인할 수 있다.
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
//Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
다른 타입의 매개변수로 제한되어 있는 상태의 매개변수도 선언할 수 있다!
이름이 있는 객체에서 인자를 가져오고 싶은 경우, 존재하지 않는 값을 가져오지 않기 위해 새로운 제약조건인
key
값으로 제약을 둘 수 있다.
범용성 높은 컴포넌트를 작성하기 위해서는 어떤 타입이던 편리하게 받아들이고, 사용할 수 있는 Generic
이 있다는 것을 배우게 되었다.
지금으로서는 ‘아니 이렇게 중요하고 좋은 걸 이제 알았다고?’ 라고 생각이 들지만, 내가 직접 사용할 수 있는 정도로 체득하기에는 어느정도의 시간이 필요할 것 같다.
기존에는 Props 하나하나에 타입을 지정해주었다면, 더 다양하게 사용할 수 있는 Type 변수의 추가로 더 활용성이 높은 컴포넌트와 코드 작성이 가능하지 않을까 싶다.