2024년 4월 30일
파생 상태(Derived state)
란 이전에 정의 되었던 상태들을 통해 계산할 수 있는 상태를 의미한다.
즉, 이전 정보로 계산할 수 있는 정보는 상태에 넣지 않고. 필요한 상태들만 최소한으로 관리하는 것이다.
변수를 상태에 저장하기보다, 변경사항이 생길때의 데이터를 동기화 상태로 유지하는것이 더 간단하다.
interface OptionsType {
[key: string]: string[];
}
const GENRES = ['KPOP', 'Hiphop', 'Indie'];
const Options: OptionsType = {
KPOP: ['Impossible', 'Kingdomcome', 'Wish'],
Hiphop: ['False Prophets', 'Midnight', 'Waves'],
Indie: ['Surf', 'Abeja'],
};
const GenreOption = ({ genre }: { genre: string }) => {
return (
<div style={{ display: 'flex' }}>
<input type={'checkbox'} />
<h3>{genre}</h3>
</div>
);
};
const SongOption = ({ song }: { song: string }) => {
return (
<div>
<input type={'checkbox'} />
<span>{song}</span>
</div>
);
};
export default function DerivedState() {
return (
<div>
{GENRES.map((genre) => {
return (
<>
<GenreOption genre={genre} />
{Options[genre].map((song) => {
return <SongOption song={song} />;
})}
</>
);
})}
</div>
);
}
위와 같은 상태가 있다고 가정해보자.
KPOP
, Hiphop
, Indie
총 3개로 장르가 나누어져있고,
그 장르에 맞는 곡들에 관한 배열 객체가 존재한다.
여기서 원하는것은 장르 자체에 대한 체크박스를 클릭했을때는 해당 하위의 모든 체크박스들이 체크가 되어야한다.
하지만 각 개별 노래에 대한 체크박스를 클릭한다면 해당하는 노래만 체크 되어야 한다.
어떻게 상태 관리를 해주어야할까 ?
개별 선택된 노래들에 대한 상태와, 장르 자체를 선택하는 상태 2개를 둠으로써 관리하려고 할 수 있다.
const [selectedOption, setSelected] = useState(Options);
const [isSelectedAllOptions, setIsSelectedAllOptions] = useState(false);
하지만 공식문서 리액트로 사고하기 섹션을 보면 컴포넌트 안에 다른 state나 props를 가지고 계산 가능한 값이 존재한다면, 그것은 state
로 관리할 필요가 없다고 기재되어있다.
위의 예시에서는 각 노래에 대한 상태 값만 가지고 있다면 전체 장르 체크에 대한 상태는 굳이 state
로 선언해주지 않더라도 관리가 가능하다.
import { useState } from 'react';
interface OptionsType {
[key: string]: string[];
}
interface SongSelectionsType {
[song: string]: boolean;
}
const GENRES = ['KPOP', 'Hiphop', 'Indie'];
const Options: OptionsType = {
KPOP: ['Impossible', 'Kingdomcome', 'Wish'],
Hiphop: ['False Prophets', 'Midnight', 'Waves'],
Indie: ['Surf', 'Abeja'],
};
type GenereOptionProps = {
genre: string;
handleSelectGenre: React.ChangeEventHandler<HTMLInputElement>;
isChecked: boolean;
};
const isGenreChecked = (genre: string, songSelections: SongSelectionsType) => {
return Options[genre].every((song) => songSelections[song]);
};
const GenreOption = ({ genre, handleSelectGenre, isChecked }: GenereOptionProps) => {
return (
<div style={{ display: 'flex' }}>
<input onChange={handleSelectGenre} type={'checkbox'} checked={isChecked} />
<h3>{genre}</h3>
</div>
);
};
type SongOptionProps = {
song: string;
handleSongSelections: React.ChangeEventHandler<HTMLInputElement>;
isChecked: boolean;
};
const SongOption = ({ song, handleSongSelections, isChecked }: SongOptionProps) => {
return (
<div>
<input onChange={handleSongSelections} type={'checkbox'} checked={isChecked} />
<span>{song}</span>
</div>
);
};
export default function DerivedState() {
const [songSelections, setSongSelections] = useState<SongSelectionsType>({});
// 각각의 개별 노래를 선택하는 경우
const handleSongSelections = (e: React.ChangeEvent<HTMLInputElement>, song: string) => {
setSongSelections({ ...songSelections, [song]: e.target.checked });
};
// 장르 체크 버튼을 클릭하는 경우
const handleSelectGenre = (e: React.ChangeEvent<HTMLInputElement>, genre: string) => {
const newSongSelections = { ...songSelections };
Options[genre].forEach((song) => (newSongSelections[song] = e.target.checked));
setSongSelections(newSongSelections);
};
return (
<div>
{GENRES.map((genre, index) => {
return (
<div style={{ borderBottom: '1px solid black' }} key={index}>
<GenreOption
isChecked={isGenreChecked(genre, songSelections)}
handleSelectGenre={(e) => handleSelectGenre(e, genre)}
genre={genre}
/>
{Options[genre].map((song, index) => {
return (
<SongOption
key={index}
isChecked={Boolean(songSelections[song])}
handleSongSelections={(e) => handleSongSelections(e, song)}
song={song}
/>
);
})}
</div>
);
})}
</div>
);
}
위에서 코드를 보면 songSelections
라는 상태를 하나만 가지고 있지만, 하나의 상태로 각각의 개별 상태와 모든 경우가 체크 된 경우의 상태 2개를 관리해주는 것을 알 수 있다.
이번 페이먼츠 미션에서도 마찬가지였다.
에 대한 모든 상태 값이 유효해야지만 확인
버튼이 렌더링 되어야했다.
const useDetectComplete = ({
cardNumbers,
month,
year,
cvc,
password,
name,
}: UseDetectCompleteHookProps) => {
const [isValidAllFormStates, setIsValidAllFormStates] = useState(false);
useEffect(() => {
const totalCardNumbers = cardNumbers
.map((cardNumber: InitialCardNumberState) => cardNumber.value)
.join('');
if (
month.length === MAX_LENGTH.MONTH &&
year.length === MAX_LENGTH.YEAR &&
totalCardNumbers.length === CARD_NUMBER.TOTAL_MAX_LENGTH &&
cvc.length === MAX_LENGTH.CVC &&
password.length === MAX_LENGTH.PASSWORD &&
name.length
) {
setIsValidAllFormStates(true);
return;
}
setIsValidAllFormStates(false);
}, [month, year, cardNumbers, cvc, name, password]);
return { isValidAllFormStates };
};
export default useDetectComplete;
그래서 이런식으로 useDetectComplete
라는 커스텀 훅을 만들어주어서 각 입력값에 대한 유효성 검사를 실시간으로 진행해주었다.
하지만 훅이 아니라 아예 함수로 관리해주는것이 어떻겠냐는 리뷰어분의 피드백을 받았다.
export default function RegisterCardInfoPage() {
// cardNumbers 상태
// month 상태
// year 상태
// password 상태
// cvc 상태
// name 상태
const isValidAllFormStates = validate.isValidAllFormStates({
cardNumbers,
month,
year,
password,
cvc,
name,
});
}
그래서 이런식으로 하나의 함수로 관리해주어서 상태값이 변할때마다 유효성 검증일 진행하도록 하였다.
이러한 과정을 겪고 난 후에 파생 상태
라는 것에 대해서 공부해보니 감회가 새로웠다.
항상 state 선언 전에 이것이 정말 상태가 필요한것인가? 에 대해서 되짚어보는 연습이 필요하다고 느꼈다.