옛날에도 플러터로 라디오 토글버튼을 구현하는데 엄청 애먹었었는데
리액트로도 이걸 구현하는데 엄청 애먹어서 기록으로 남겨봅니다.
그 전에 우선 '라디오 토글 버튼'의 정의를 명확하게 하자면,
라디오버튼에 토글 기능을 넣은 버튼을 의미합니다.
기본적으로 라디오버튼은 반드시 하나가 선택되어 있어야 합니다.
저는 이미 선택된 라디오 버튼을 한번 더 선택하면 선택이 해제 되도록 만들고 싶었습니다.
<MyEnInfoSelectionGroup name='국가기술자격증'>
<MyEnInfoSelectionItem>기사 이상</MyEnInfoSelectionItem>
<MyEnInfoSelectionItem>산업기사</MyEnInfoSelectionItem>
<MyEnInfoSelectionItem>기능사</MyEnInfoSelectionItem>
</MyEnInfoSelectionGroup>
이렇게 구성하면 각 버튼은 토글 버튼인 동시에 같은 그룹 안의 컴포넌트끼리는 중복선택이 되지 않게 하려고 합니다.
영상으로 보면 이렇게 됩니다.
처음에는 각 버튼에 클릭 상태를 저장하는 변수를 만들고, 클릭할 때마다 상태를 바꿔주도록 했습니다.
처음에는 이 아이디어를 계속 끌고가면서 구현하려고 했는데, 결론적으로 이렇게 하면 안됩니다.
이렇게 하면 단일 토글 버튼을 만드는 데는 적절할 지 몰라도 라디오 토글버튼을 만드는 데는 도움이 되지 않습니다.
왜냐하면 어떤 버튼을 클릭하면 같은 그룹의 모든 버튼을 순회하면서 나머지 선택된 버튼을 해제해주어야 하는데
리액트에서는 부모 컴포넌트가 자식 컴포넌트의 상태를 변경하는 방법이 옳은 방법이 아니기 때문입니다.
자식 컴포넌트의 상태는 부모 컴포넌트에서 관리되는 것이 옳다고 합니다.
그래서 저는 선택된 값의 상태를 저장하는 변수를 부모컴포넌트 (그룹 컴포넌트) 에 두고,
자식 컴포넌트의 클릭이벤트로 부모 컴포넌트에 저장된 '선택된 값의 상태' 변수를 변경하도록 했습니다.
(이건 부모 컴포넌트의 '선택된 값의 상태' 변수의 setState 메소드를 자식 컴포넌트로 props를 통해 넘겨주면 됩니다.)
컴포넌트그룹 컴포넌트 코드
const MyEnInfoSelectionGroup = (props) => {
const [selection, setSelection] = useState(null); // 1.선택된 값의 상태를 저장하는 변수와 세터
useEffect(() => {
const new_children = React.Children.map(props.children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
selectionHandler, // 3. 자식 컴포넌트의 속성으로 넘겨줍니다.
...
});
}
return child;
});
setChildren(new_children);
}, [selection]);
};
const selectionHandler = (value) => {
setSelection(value); // 2. 선택된 값의 상태를 변경할 세터를 함수로 만들어주고
};
참고로 이렇게 리액트에서 컴포넌트에 속성을 동적으로 추가할 땐
컴포넌트를 전부 복사해서 추가해야한다고 합니다.
그냥 자바스크립트 오브젝트에 추가하듯 추가하면 확장 불가능한 객체라는 에러가 나오더라구요.
라디오토글버튼 컴포넌트
const MyEnInfoSelectionItem = (props) => {
const handleClick = (e) => {
if (props.clicked) {
props.selectionHandler(null); // 넘겨받은 세터 함수로 부모 컴포넌트의 상태 변경
}
else {
props.selectionHandler(props.children);
}
};
return (
<div className={'en-info-edit-list-item' + (props.clicked ? ' clicked' : '')} onClick={handleClick}>
{props.children}
</div>
);
};
이제 버튼을 클릭해서 부모의 상태가 변경되었다면, 이걸 감지해서 토글버튼의 클릭 여부를 바꿔주어야 합니다.
상태 변경을 감지하는 건 useEffect() 메소드를 이용하면 되는건 쉽게 이해할 수 있었습니다.
그러나 토글 버튼의 클릭 여부를 어떻게 바꿔 주어야 할지가 어려웠습니다.
이건 모든 자식 컴포넌트들을 순회하면서 각 토글 버튼마다 변하지 않는 클릭 여부 속성값을 다시 설정해준 뒤
자식 컴포넌트 전체를 다시 그려버리면 됩니다.
자식 버튼의 클릭 여부를 부모 컴포넌트에서 수시로 바꿔줄 필요가 있기 때문에
자식 버튼의 클릭 여부는 자식 버튼이 상태 데이터로 갖는게 아니라
부모 컴포넌트가 결정해서 넘겨주는 것이라고 이해하면 됩니다.
버튼 그룹 컴포넌트
const MyEnInfoSelectionGroup = (props) => {
const [selection, setSelection] = useState(null);
const [children, setChildren] = useState(props.children);
useEffect(() => {
const new_children = React.Children.map(props.children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
selectionHandler,
clicked: child.props.children === selection // 클릭 여부를 다시 세팅해서 자식 컴포넌트들을 복사합니다.
});
}
return child;
});
setChildren(new_children); // 복사한 컴포넌트로 다시 화면을 그립니다.
}, [selection]);
const selectionHandler = (value) => {
setSelection(value);
};
return (
<li>
{props.name}
<div className='en-info-edit-list-container'>
{children}
</div>
</li>
)
};
라디오버튼 컴포넌트 코드는 변한 게 없습니다.