09.23.2023
1. 문제 발생
React DnD를 이용해서 드래그 앤 드랍 기능을 이용하던 도중 알 수 없는 문제를 마주쳐 거의 하루를 고민했던 것 같다.
Page.tsx
import { useState } from 'react';
import DragTargetLine from '/my/path';
interface Activity {
id: number;
planId: number;
}
const testData: Activity = {
id: 1,
planId: 1,
};
function Plan(): JSX.Element {
const [activities, setActivities]: (Activity : object)[] = Array.from({ length: 24 }, () => ({}));
const handleDropComponent = (draggedActivity: Activity, targetLine: number) => {
console.log('activities: ', activities);
const updatedActivity: Activity | undefined | object = activities.find(
(activity) => 'id' in activity && activity.id === draggedActivity.id,
);
};
useEffect(() => {
const newactivities = [...activities];
newactivities[testData.id] = testData;
setActivities(newactivities);
});
return <DragTargetLine
lineId={index}
onDropComponent={handleDropComponent}
/>;
}
1-1. 코드설명
Activity타입의 객체는 Draggable, dragTargetLine은 Droppable이다.
페이지가 렌더링 되면 activities는 빈 24개의 오브젝트들로 initialized 된다.
그 후 useEffect에서 testData의 id인 1번째 activities에 삽입된다.
화면에는 DragTargetLine이 나타날거고, handleDropComponent가 인자로 들어간다.
handleDropComponent는 드래그 후 드랍되었을 때 실행되는 함수이며, activities에서 드래그된 액티비티를 찾는 일을 수행한다.
1-2. 에러설명
나는 activities가 testData를 포함할 거라고 생각했다.
그러나 저 코드를 실행시키면 실제로 activities는 모두 빈 오브젝트로 출력된다.
그렇기 때문에 updatedActivity의 값은 undefined가 되어버린다.
2. 원인
이 에러는 결론적으로 리액트의 특성을 몰랐기 때문에 마주한 문제였다.
2-1. 리액트의 이벤트 핸들러
리액트에서는 컴포넌트가 렌더링될 때, 이벤트 핸들러(함수)가 재생성된다.
중요한 것은 이벤트 핸들러가 재생성되었을 때, 각 버전의 이벤트 핸들러는 컴포넌트가 렌더링 된 그 시점의 상태와 프로퍼티(States and Props)를 기억한다는 것이다.
쉽게 말해 리액트에서는 리렌더링이 발생하지 않으면, 이벤트 핸들러는 영원히 해당 컴포넌트가 가장 처음 렌더되었을 시점의 상태와 프로퍼티만을 기억한다는 것이다.
나의 코드에서는 Drag and Drop이 일어나더라도 리렌더링이 되지 않는 것을 확인했고, 리렌더링 없이 최신 상태를 가져와야만 했다.
이러한 문제의 해결법은 무엇이 있을까?
3. 해결방법
3-1. useRef
useState를 사용하는 경우 상태를 항상 최신값을 유지하게 할 수 있는 useRef를 통해 해결을 할 수 있다.
예제코드.tsx
import { useRef } from 'react';
const activitiesRef = useRef<(Activity | object)[]>(activities);
const handleDropComponent = (draggedActivity: Activity, targetLine: number) => {
const currentActivities = activitiesRef.current;
const updatedActivity: Activity | undefined | object = currentActivities.find(
(activity) => 'id' in activities && activity.id === draggedActivity.id,
);
}
드래거블 액티비티는 항상 activities안에 저장되어있는 것이 보장되므로, updatedActivity의 타입에서 undefined를 지워주었다
3-2. RTK
리덕스 툴킷을 사용하는 경우, useState를 사용하는 대신 slice를 만들어 전역변수로 지정하고 최신 데이터를 불러오는 방법이 있다.
이미 RTK를 쓰고 있었으므로 나는 이 방법을 선택했고, 코드는 밑 결과 섹션에서 볼 수 있다.
4. 결과
activitiesSlice.tsx
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
interface Activity {
id?: number;
planId: number;
}
const initialState: (Activity | object)[] = Array.from(
{ length: 24 },
() => ({}),
);
const activitiesSlice = createSlice({
name: 'activities',
initialState,
reducers: {
setActivities: (state, action: PayloadAction<(Activity | object)[]>) => {
return action.payload;
},
},
});
export const { setActivities } = activitiesSlice.actions;
export default activitiesSlice.reducer;
store.tsx
import { configureStore } from '@reduxjs/toolkit';
import activitiesReducer from './Slices/activitiesSlice';
const store = configureStore({
reducer: {
activities: activitiesReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
Plan.tsx
import { useState } from 'react';
import store from 'store/path';
import DragTargetLine from '/my/path';
interface Activity {
id: number;
planId: number;
}
const testData: Activity = {
id: 1,
planId: 1,
};
function Plan(): JSX.Element {
const handleDropComponent = (draggedActivity: Activity, targetLine: number) => {
const currentActivities = store.getState().activities;
const updatedActivity: Activity | undefined | object = currentActivities.find(
(activity) => 'id' in activity && activity.id === draggedActivity.id,
);
};
useEffect(() => {
const newactivities = [...activities];
newactivities[testData.id] = testData;
setActivities(newactivities);
});
return <DragTargetLine
lineId={index}
onDropComponent={handleDropComponent}
/>;
}
'Project > Time Map' 카테고리의 다른 글
[React] Modal을 재사용 가능한 컴포넌트로 리팩토링하기 (0) | 2023.08.30 |
---|---|
메모이제이션이란?(useMemo, useCallback) in React.js (0) | 2023.06.26 |