리액트 리팩토링
useEffect 사용
- props, state 변경에 따라 다른 데이터, state를 업데이트해야할 때 useEffect의 사용을 자제하자.
- useEffect vs EventHandler 중 어느 쪽에서 유저 인터랙션을 처리해야할까?
- useEffect 내에서 data fetching을 할 때 주의해야할 점.
race condition 문제
주로 API 요청을 보낼 때 발생함. 요청을 여러 번 보낼 때, 앞 요청이 끝나기 전에 뒤 요청이 먼저 끝날 수도 있다. 이러한 상황을 항상 고려하자.
예시
- “true”란 응답이 올 때까지 앞 요청에서 “false”란 응답이 왔는데, 뒷 요청에서 “true”란 응답이 온다고 해보자. 뒷 요청이 먼저 끝나면 나중에 오는 앞 요청의 응답이 잘
Silent Error를 없애자
1
2
3
4
5
6
7
8
9
10
11
12
const onClickCorrect = async () => {
if (
selectedUserState !== null
) {
try {
const res = await applyForUser({selectedUserState})
setApplicationId(res.applicationId);
navigate('/waiting');
} catch (e) {
console.log(e);
}
}};
- 위와 같이 클릭 시 발동되는 이벤트핸들러가 있다고 해보자. 만약
selectedUserState
가 null 일 경우 유저는 아무리 클릭을 해도 아무런 반응을 얻지 못할 것이다. - 따라서
else
문을 추가하여selectedUserState
가 null 일 경우를 대비하자.
새로고침, 뒤로가기 고려
컴포넌트가 새로고침이나 뒤로가기를 통해 다시 마운트 될 시, 고려 해야 하는 부분이 몇 가지 있다.
- 상태가 초기화 된다.
- useEffect가 실행된다.
- 컴포넌트 내에서 react-query 사용 시, refetch가 이루어진다.
아래 코드를 보자. 컴포넌트가 마운트 되면, react-query는 유저타입 데이터를 받아온다. 그리고 useEffect 에서는 그 중 첫 번째 타입을 selectedUserState 상태에 설정한다.
유저타입 데이터는 선택된 상태로 렌더링 되고, 유저가 리스트에서 원하는 Row를 선택하면 그 타입으로 selectedUserState 를 업데이트한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export function CreditCardTypePage() {
const { data: userTypes } = useQuery(reactQueryKey.USER_TYPE, fetchUserTypes);
const [{ userType: selectedUserType }, setSelectedUserState] = useRecoilState(recoil_SelectedUserState);
useEffect(() => {
if (userTypes !== undefined) {
setSelectedUserState(prev => ({ ...prev, userType: userTypes[0] }));
}
}, [userTypes, setSelectedUserState]);
const onClickUserTypeRow = (userType: UserType) => {
setSelectedUserState(prev => ({ ...prev, userType }));
};
return () => {
<List>
{userTypes?.map(item => (
<ListRow.Selectable
key={item.id}
contents={<ListRow.Text1Row top={item.title} />}
right={<Checkbox readOnly={true} checked={selectedUserType !== null && item.id === selectedUserType.id} />}
onClick={() => onClickUserTypeRow(item)}
/>
))}
</List>
}
}
이 코드에는 새로고침, 뒤로가기 시 유저가 선택한 상태가 초기화 되어 첫 번째 Row가 선택된 상태로 렌더링 된다는 문제점이 존재한다. 이러한 문제점을 해결하기 위해서는 다음과 같이 처리 해주어야 한다.
- recoil-persist 등을 활용하여 상태를 로컬스토리지에 저장하고, 페이지가 리로드 될 시 상태를 로컬에 저장된 상태로 초기화한다.
useEffect
내에서 상태를 업데이트할 때,selectedUserType
이 null일 때만 업데이트하도록 한다.
1번을 통해 새로고침 되어 상태가 전부 초기화 되었을 때를 대응하고, 2번을 통해 유저가 선택된 상태를 유지하도록 한다.
중복코드 분리
위 코드 예시에서 보여 준 컴포넌트는 recoil_SelectedUserState
에서 userType
데이터만 사용한다. 만약 recoil_SelectedUserState
가 다음과 같이 작은 상태이고 한 부분에서만 사용한다면 큰 문제는 없다.
1
2
3
4
5
6
7
8
9
10
11
12
const recoil_SelectedUserState = atom<{
userType: UserType | null;
userDesign: UserDesign | null;
userFields: SelectedUserField[] | null;
}>({
key: 'selectedUserState',
default: {
userType: null,
userDesign: null,
userFields: null,
},
});
하지만 만약 userType
만, userDesign
만, userFields
만 따로 사용하는 부분이 반복된다면 어떻게 될까? 만약 새로고침과 뒤로가기에 대응하느라 그러한 부분들을 전부 같은 형식으로 수정해야 한다면 어떻게 될까?
이럴 때가 바로 중복되는 부분을 분리해줘야 할 때다.
같은 부분을 두 번 이상 반복하고 있다면, 바로 그때가 코드를 분리해야 할 때다.
이러한 부분은 hook이나 컴포넌트로 분리할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// hook
const reactQueryPropsByUse = {
"userType" : {
reactQueryKey : reactQueryKey.USER_TYPE,
fetch : fetchUserTypes,
},
}
export function SingleSelectedUserState({use = "userType", defaultValue = null}) {
const { data } = useQuery(reactQueryPropsByUse[use].reactQueryKey, reactQueryPropsByUse[use].fetch);
const [{ [use]: selectedUserData }, setSelectedUserState] = useRecoilState(recoil_SelectedUserState);
useEffect(() => {
if (data !== undefined && selectedUserData === null && defaultValue === null) {
setSelectedUserState(prev => ({ ...prev, [use]: data[0] }));
}
if(defaultValue !== null) {
setSelectedUserState(prev => ({ ...prev, [use]: defaultValue ] }));
}
}, [data, selectedUserData, use, defaultValue, setSelectedUserState]);
const setSingleSelectedUserState = (userData) => {
setSelectedUserState(prev => ({...prev, [use] : userData}));
}
return [selectedUserData, setSingleSelectedUserState];
}
컴포넌트로 분리할 때는 원하는 로직을 따로 뺀 후, children으로 그 로직을 사용할 컴포넌트를 불러오면 된다.
1
2
3
4
5
6
7
8
const ModalLayout = (props) => {
const {children, setContent, isOpenModal} = props;
return (
<ModalBox>
{React.cloneElement(children, {setContent, isOpenModal})}
</ModalBox>
)
}
참고
https://velog.io/@hyunjoogo/React-children-Component%EC%97%90-props-%EC%A0%84%EB%8B%AC%ED%95%98%EA%B8%B0