지금까지는 Uncontrolled Component로 만들었었는데, 라디오는 Controlled Component로 만들어보려고 한다. 사실 이렇게 만들었어야 했는데 결심한 것처럼 말하기ㅋㅋ
그러서는 Radio Button의 특성상 Group으로 묶어주는 역할이 필요하고, 그 Group과 자식들이 값을 공유해야 한다. 이를 위해 Context를 만들어 공유해주었고, Group 컴포넌트를 만들었다.
// RadioContext.tsx
type RadioContextProp = {
disabled?: boolean;
value?: boolean;
onChange?: (value: string) => void;
};
const RadioContext = createContext<RadioContextProp>({});
// RadioGroup.tsx
type RadioGroupProp = {
label: string;
children: React.ReactNode;
disabled?: boolean;
value?: boolean;
onChange?: (value: string) => void;
};
const RadioGroup = ({ label, children, ...rest }: RadioGroupProp) => {
return (
<fieldset>
<legend>{label}</legend>
<RadioContext.Provider value={rest}>{children}</RadioContext.Provider>
</fieldset>
);
};
// RadioButton.tsx
type RadioButtonProp = {
id: string;
name: string;
value: string;
children?: React.ReactNode;
defaultChecked?: boolean;
disabled?: boolean;
};
const RadioButton = ({
id,
name,
value,
children,
defaultChecked,
disabled = false,
}: RadioButtonProp) => {
const group = useContext(RadioContext);
const radioShape =
"peer appearance-none rounded-full border-m1 border-black bg-lightgray " +
"tablet:border-t1 desktop:border-d1 ";
const radioLayout =
"grid place-content-center aspect-square w-m24 min-w-m24 h-m24 min-h-m24 " +
"tablet:w-t24 tablet:h-t24 tablet:min-w-t24 tablet:min-h-t24 " +
"desktop:w-d24 desktop:h-d24 desktop:min-w-d24 desktop:min-h-d24 ";
const radioBefore =
"before:content-[''] " +
"before:block " +
"before:w-m14 " +
"tablet:before:w-t14 desktop:before:w-d14 " +
"before:h-m14 " +
"tablet:before:h-t14 desktop:before:h-d14 " +
"before:rounded-full " +
"before:bg-black " +
"before:hidden ";
const radioState =
radioBefore +
"checked:before:block " +
"disabled:border-gray disabled:before:bg-gray";
const radioStyle = radioShape + radioLayout + radioState;
useEffect(() => {
if (defaultChecked && group.onChange) {
group.onChange(value);
}
}, []);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
if (group.onChange) {
group.onChange(e.target.value);
}
};
return (
<label
className="flex w-fit flex-row items-center gap-m12 tablet:gap-t12 desktop:gap-d12"
htmlFor={id}
>
<input
id={id}
className={radioStyle}
type="radio"
name={name}
value={value}
defaultChecked={defaultChecked || false}
disabled={disabled || group.disabled}
checked={group.value && group.value}
onChange={handleChange}
/>
<span className="caption peer-disabled:text-gray">{children}</span>
</label>
);
};
저번 TextIconButton을 만들 때는 pointerhover:hover:안에 before를 계속 써줬었는데 넘 번거롭기도 했고 코드가 겁나 길어져서 보기가 싫었다(ㅋㅋ). 그래서 before를 생성해준 다음에 display로 제어만 해줬다. 가운데 정렬은 grid로 place-content-center 먹여주었다. 단순히 가운데 정렬만 할 거면 그냥 grid의 저 속성 쓰는 게 더 나은 것 같기도 하고.
나는 flag를 지정해주듯 속성만 써주는 걸 좋아한다. 이쁘잖아! 그런데 내가 생각을 잘못하고 있었던 게 있었다.
아래와 같은 코드가 있다고 가정하자.
type MyComponentProp = {
hidden?: boolean;
}
const MyComponent = ({hidden}: MyComponentProp) => {
~
}
<!-- (1) --> <MyComponent />
<!-- (2) --> <MyComponent hidden />
<!-- (3) --> <MyComponent hidden={true} />
hidden prop은 boolean이지만 생략이 가능한 optional로 선언이 되어있다. 그러면~ JSX에서 (1)의 형태처럼 hidden 속성을 쓰지 않으면 undefined이 반환 된다고는 알고 있었는데, (2)처럼 만약 hidden만 써놨다면 이 값에 들어가는 게 false라고 생각했었다. 당연히 undefined는 아니고, 값을 지정해주지 않았으니 null이든 뭐든 들어갈 텐데, 이게 boolean형이니까 false가 될거라고 생각했다. 그래서 쓰려면 (3)의 형태로 쓰는 게 맞다고 생각했었는데 ,,
얼레
그냥 hidden만 써주는 것만으로도 true가 되었다. 이럴 수가!
갑자기 boolean이 아닌 다른 타입으로 해도 되는 건지 궁금해졌다.
기본적으로 속성이 들어있으면 true를 반환하는데, 이게 boolean이 아닌 다른 타입이라면 TS에서 문제를 일으킨다. 재밌군 ..
function App() {
const [selectedValue, setSelectedValue] = useState("");
const handleChange = (value: string) => {
setSelectedValue(value);
};
return (
<>
<RadioGroup label="연락처를 선택하세요" onChange={handleChange}>
<RadioButton
name="contact"
id="contact-1"
value="1ST-EMAIL"
defaultChecked
>
1st contact
</RadioButton>
<RadioButton name="contact" id="contact-2" value="2ND-EMAIL">
2nd contact
</RadioButton>
</RadioGroup>
<p>현재 선택된 Value: {selectedValue}</p>
</>
);
}
만들다보니 느끼는 건데, tailwind는 atomic desigin에서는 확실히 불편한 점이 많은 것 같다. UI 컴포넌트를 만들어서 쓸 생각을 하지 않고 그냥 쓱쓱쓱 진행한다면 정말 편하고 빠른 것이 장점이지만, 의사 클래스(pesudo-classes)를 사용하거나 복잡한 media query를 사용하게 되면 코드를 관리하기 넘 힘들어지는 것 같다😇
만약에 tailwind에서도 의사 클래스나 미디어 쿼리에서 많은 속성을 쓸 수 있게 만들어 준다면 정말 괜찮을 것 같기도 한데! 예를 들어 뭐,, 이런 식으로.
tablet:[w-t24 border bg-black hover:[border-white bg-white text-black] before:[content-none w-12 h-12 rounded-full]]
넘 복잡해지나?ㅋㅋ 머.. 최근에 나온 WindiCSS던가? 그건 이런 식으로 사용이 가능하긴 하더라. 나중에 써보긴 할 텐데,, 난 이제 tailwind처럼 클래스에 넣는 방식이 아니라 Styled Component 형식으로 접근하는 게 더 좋은 것 같다. emotion이나 더 파야지.