이번에 프로젝트를 진행하면서 useState()를 활용한 상태관리를 하고 있는데, 상태관리에 대한 확실한 이해 없이 무작정 작업을 하다보니 코드가 내가 이해하기도 어려울 정도로 복잡해져 있었다. 그래서 상태관리에 대한 공부를 하고 다시 코드를 수정했다. 이번에 알게 된 내용을 한 번 정리해 보려 한다.
1. 상태 관리는 부모에서 이루어지고, 자식에게 props로 전달한다.
부모 컴포넌트 A와 자식 컴포넌트 B,C가 있다고 가정하자.
그렇다면 상태를 관리하는 로직은 부모 컴포넌트 A에 작성하고 이를 자식한테 props로 전달하면 된다.
만약 자식 컴포넌트에서 버튼을 클릭하면 상태가 업데이트되는 것을 부모한테 전달하고 싶다고 가정하자.
(여기서 부모는 SearchResult.tsx이고, 자식은 BtmSheetOption.tsx이다.)
// SearchResult.tsx
import React, { useState } from 'react';
import BtmSheetOption, { initialOptionData } from './BtmSheetOption';
const SearchResult: React.FC = () => {
const [OptionData, setOptionData] = useState(initialOptionData);
const handleOptionClick = (index: number) => {
const newOptionData = OptionData.map((item, i) => {
if (i === index) {
return { ...item, isChecked: !item.isChecked };
}
return item;
});
setOptionData(newOptionData);
};
return (
<BtmSheetOption OptionData={OptionData} handleOptionClick={handleOptionClick} />
);
};
export default SearchResult;
SearchResult.tsx에서 상태인 OptionData와 함수인 handleOptionClick을 생성하고, 자식인 BtmSheetOption에게 props로 전달한다.
// BtmSheetOption.tsx
import React from 'react';
import styles from './BtmSheetOption.module.css';
import CheckFilter from '../../../CheckFilter/CheckFilter';
interface Props {
closeBottomSheet: () => void;
OptionData: OptionData[];
handleOptionClick: (index: number) => void;
}
const BtmSheetOption: React.FC<Props> = ({ closeBottomSheet, OptionData, handleOptionClick }: Props) => {
// ... 기존 코드 ...
};
그리고 자식 컴포넌트에서는 그 props들을 받아서 사용하면 된다.
2. 상수를 변경하려하지 말고, 상태를 변경하자.
import React, { useState } from 'react';
import styles from './BtmSheetOption.module.css';
import CheckFilter from '../../../CheckFilter/CheckFilter';
type OptionData = {
title: string;
isChecked: boolean;
};
const OptionDataArr = [
{
title: '이벤트 중',
isChecked: true,
},
{
title: '쿠폰 사용 임박',
isChecked: true,
},
];
const BtmSheetOption = () => {
let initialOptionData = OptionDataArr;
const [OptionData, setOptionData] = useState(initialOptionData);
const handleOptionClick = (index: number) => {
const newOptionData = OptionData.map((item, i) => {
if (i === index) {
return { ...item, isChecked: !item.isChecked };
}
return item;
});
setOptionData(newOptionData);
};
return (
<div className={styles.wrapOption}>
{OptionDataArr.map((data, index) => (
<CheckFilter
key={index}
label={data.title}
isChecked={data.isChecked}
onClick={() => handleOptionClick(index)}
/>
))}
</div>
);
};
export default BtmSheetOption;
위 코드는 잘 실행되지 않는다. handleOptionClick을 백번 천번 실행해도 배열은 바뀌지 않는다.
왜 그럴까? 바로 밑의 부분에서 코드를 잘못 작성했다.
{OptionDataArr.map((data, index) => (
<CheckFilter
key={index}
label={data.title}
isChecked={data.isChecked}
onClick={() => handleOptionClick(index)}
/>
))}
</div>
OptionDataArr는 상수 배열이므로, 그 값은 변경되지 않는다.
코드를 살펴보면 useState 훅을 사용하여 OptionData라는 새로운 상태를 만들었고, 이 상태는 OptionDataArr의 초기 값을 가지고 있다. handleOptionClick 함수에서는 OptionData 상태만 변경하고 있다.
따라서, 클릭을 하면 OptionData의 isChecked 값만 변경되고, OptionDataArr의 isChecked 값은 변경되지 않는다.
그리고 렌더링할 때 OptionData 대신 OptionDataArr를 사용하고 있다.
이 부분을 OptionData로 변경해야 클릭할 때마다 상태 변경이 UI에 반영된다.
최종적으로 OptionDataArr를 initialOptionData라고 배열 이름을 바꾸고, 이를 useState의 초기 값으로 사용했다.
그리고 OptionData라는 상태를 사용해서 상태를 변경했다.
const initialOptionData = [
{
title: '이벤트 중',
isChecked: true,
},
{
title: '쿠폰 사용 임박',
isChecked: true,
},
];
const BtmSheetOption = () => {
const [OptionData, setOptionData] = useState(initialOptionData);
const handleOptionClick = (index: number) => {
const newOptionData = OptionData.map((item, i) => {
if (i === index) {
return { ...item, isChecked: !item.isChecked };
}
return item;
});
setOptionData(newOptionData);
};
return (
<div className={styles.wrapOption}>
{OptionData.map((data, index) => (
<CheckFilter
key={index}
label={data.title}
isChecked={data.isChecked}
onClick={() => handleOptionClick(index)}
/>
))}
</div>
);
};
export default BtmSheetOption;
3. Props 전달 형식(in Typescript)
부모 컴포넌트에서는 아까 언급한 바와 같이 useState(), 함수(onClick)를 작성하고 이를 props로 자식 컴포넌트에 전달한다. (배열도 따로 파일을 생성해서 작성하지 않았다면, 자식 컴포넌트가 아닌 부모 컴포넌트에 작성하는 것이 좋다.)
// 부모.tsx
import React, { useState } from 'react';
import styles from './부모.module.css';
import 자식 from './자식';
type OptionData = {
title: string;
isChecked: boolean;
};
const initialOptionDataArr = [
{
title: '이벤트 중',
isChecked: true,
},
{
title: '쿠폰 사용 임박',
isChecked: true,
},
];
const 부모 = () => {
// 자식 컴포넌트 상태 관리
const [OptionData, setOptionData] = useState(initialOptionDataArr);
// 클릭했을 때의 함수
const handleOptionClick = (index: number) => {
const newOptionData = OptionData.map((item, i) => {
...
});
setOptionData(newOptionData);
};
return (
<div>
...
<자식
onClick={handleOptionClick}
OptionDataArray={OptionData}
/>
...
</div>
);
};
export default 부모;
자식 컴포넌트에서는 Props interface를 만들어서 전달될 Props들의 형식을 지정해준다.
그리고 Props를 전달받아야 하므로 아래와 같은 방식으로 코드를 작성해준다.
const 자식: React.FC<Props> = ({
전달받은 Props1
전달받은 Props2
...
} : Props) => {
....
최종적으로 작성한다면 아래와 같은 코드가 된다.
// 자식.tsx
import React from 'react';
import styles from './자식.module.css';
import CheckFilter from '../../../CheckFilter/CheckFilter';
type OptionData = {
title: string;
isChecked: boolean;
};
interface Props {
/**
* 전달된 배열
*/
OptionDataArray: OptionData[];
/**
* 버튼 클릭할 때
*/
onClick: (index: number) => void;
}
const 자식: React.FC<Props> = ({
OptionDataArray,
onClick,
}: Props) => {
return (
<div className={styles.wrapOption}>
{OptionDataArray.map((data, index) => (
<CheckFilter
key={index}
label={data.title}
isChecked={data.isChecked}
onClick={() => onClick(index)}
/>
))}
</div>
);
};
export default 자식;
'Frontend > React' 카테고리의 다른 글
'react-datepicker'에서 날짜 상태를 관리할 때 겪었던 시행착오 (0) | 2024.08.03 |
---|---|
프로젝트 팀원 코드를 보고 컴포넌트 만드는 법 배우기(typescript + clsx) (0) | 2024.07.21 |
[React] useEffect()에 대해서 (0) | 2023.08.17 |
[React] Props에 대해서 (0) | 2023.08.17 |
[React] useState()에 대해서 (0) | 2023.08.17 |