React에서 map으로 펼쳐놓은 각각의 JSX요소에 각각에 이벤트를 적용해주고 싶을 때까 있다.
이럴 때 보통 해당 요소의 onClick과 같은 이벤트를 사용하여, 구현을 하게되는데 무심코 코딩을 하다보면 다음과 같은 상황을 마주하게된다.
모든 요소가 같은 상태값을 참조하고 있기 때문에, 여러 버튼 중 한 버튼만 클릭이 되더라도 모든 요소가 다 반응하는 것이다.
해당 문제를 발생 시키는 코드는 아래와 같다.
const [colorPickerVisible, setColorPickerVisible] = useState(false);
...
<Box
display='flex'
flexDirection='column'
gap={2}
width='95%'
margin='auto'
>
{categoryList.slice(1).map((option,index) => (
<Box key={index} value={option.value}>
<Box
display='flex'
alignItems='center'
justifyContent='space-between'
>
<Paper
sx={{
width: "10px",
height: "10px",
backgroundColor: option.value,
}}
></Paper>
<Typography sx={{ flex: 1 }}>
{option.label}
</Typography>
<Box
position='relative'
display='flex'
alignItems='center'
>
//핵심 구간 시작
<Avatar
alt='colorPicker'
src={colorPicker}
sx={{
height: "20px",
width: "20px",
cursor: "pointer",
"&:hover": {
transform: "scale(1.2)",
},
// position: "absolute",
top: "0",
left: "0",
}}
onClick={() => setColorPickerVisible((prev) => !prev)}
/>
<SketchPicker
color={option.value}
className={
colorPickerVisible
? "colorPickerVisible"
: "colorPickerInvisible"
}
/>
//핵심 구간 끝
<DeleteIcon
sx={{
color: "darkGrey",
marginLeft: "10px",
cursor: "pointer",
"&:hover": {
transform: "scale(1.2)",
// outline: "none",
},
}}
/>
</Box>
</Box>
</Box>
))}
</Box>
이를 해결하기 위해서는 어떻게 해야할까?
colorPickerVisible 상태를 배열로 관리하고, 각 Avatar 컴포넌트의 인덱스를 해당 상태 배열에서 참조하여 특정 SketchPicker만 보이도록 변경하면 된다.
이런식으로 말이다.
const [categoryList, setCategoryList] = useState([
{ id: 0, value: "#fff", label: "전체" },
{ id: 1, value: "#9DC08B", label: "개인" },
{ id: 2, value: "#40513B", label: "직장" },
{ id: 3, value: "#609966", label: "가족" },
{ id: 4, value: "#719192", label: "생일 및 기념일" },
]);
const [colorPickerVisible, setColorPickerVisible] = useState(
categoryList.slice(1).map(() => false)
);
<Box>
{categoryList.slice(1).map((option, index) => (
<Box key={index} value={option.value}>
<Box>
<Avatar
onClick={() => {
const newVisible = [...colorPickerVisible];
newVisible[index] = !newVisible[index];
setColorPickerVisible(newVisible);
}}
/>
<SketchPicker
color={option.value}
className={
colorPickerVisible[index] ? "colorPickerVisible" : "colorPickerInvisible"
}
/>
</Box>
</Box>
))}
</Box>
코드를 풀어서 한번 봐보자.
먼저 선언된 변수를 먼저 보면
useState(
categoryList.slice(1).map(() => false)
);
categoryList에서 첫번째 요소를 제외한 배열의 개수만큼 false값을 맵해주고 입니다.
colorPickerVisible의 결과를 출력해본다면 [false, false, false, false]
가 나올것이다.
다음 의 onClick 이벤트 부분을 보자.
<Avatar
onClick={() => {
const newVisible = [...colorPickerVisible];
newVisible[index] = !newVisible[index];
setColorPickerVisible(newVisible);
}}
/>
먼저 colorPickerVisible를 스프레드 연산자로 새로운 배열로 만들어, newVisible이라는 변수에 할당한다.
그 다음 내가 컨택한 요소의 index값을 참고하여 배열을 해당 요소에 접근한다.
그 요소는 현재 역시나 false 일 것이다.
그것을 !false 해준 후 newVisible[index]에 할당한다.
이 시점에서 newVisible을 출력해보면
[false, true, false, false]
이런 상태일 것이다.
그리고 그 값을 setColorPickerVisible에 넣어 상태을 변경해준다.
<SketchPicker
color={option.value}
className={
colorPickerVisible[index] ? "colorPickerVisible" : "colorPickerInvisible"
}
/>
className에 조건문을 이렇게 걸어놓으면, 해당 요소는 해당 요소의 인덱스만을 참조하기 때문에, true바 바뀐 녀석만 이벤트가 실행되게 되는 것이다.
아주 잘 동작한다.