React ๋ฒ๊ทธ ํด๋ถ ์๋ฆฌ์ฆ
- [1ํธ] React ํ์ด๋จธ ์ฑ์์ stale closure์ ์ธ ๋ฒ ๋นํ ์ด์ผ๊ธฐ
- [2ํธ] useCallback ์๋ ํจ์๊ฐ useEffect๋ฅผ ๋งค ๋ ๋๋ง๋ค ์คํ์ํจ ์ด์ผ๊ธฐ
- [3ํธ] ๋ฐฑ๊ทธ๋ผ์ด๋ ํญ์์ setInterval ํ์ด๋จธ๊ฐ ๋๋ ค์ง๋ ์ด์ ์ ๋ฒฝ์๊ณ ํด๋ฒ
- [4ํธ] ๋๋๊ทธ ์ค ๋๋กญ์ด ์กฐ์ฉํ ์ฃฝ์๋ค โ react-beautiful-dnd์ React 18 StrictMode โ ํ์ฌ ๊ธ
TimeTrack์ ํ ์ผ ๋ชฉ๋ก์ ๋๋๊ทธ๋ก ์์๋ฅผ ๋ฐ๊ฟ ์ ์๊ฒ ๋ง๋ค์ด ๋์๋ค. ๊ทธ๋ฐ๋ฐ ์ด๋ ๋ ํญ๋ชฉ์ ์ก๊ณ ๋์ด๋ ์๋ฌด ์ผ๋ ์ผ์ด๋์ง ์์๋ค. ํญ๋ชฉ์ด ์์ ๋ฐ๋ผ ์์ง์ด์ง๋, ์์๊ฐ ๋ฐ๋์ง๋ ์์๋ค. ๋ ์ด์ํ ๊ฑด ์ฝ์์ ์๋ฌ๊ฐ ์์๋ค๋ ์ ์ด๋ค. ํด๋ฆญยท์์ ยท์ญ์ ๊ฐ์ ๋ค๋ฅธ ๋์์ ๋ฉ์ฉกํ๋ค. ๋๋๊ทธ๋ง ์ฃฝ์ด ์์๋ค.
์๋ฌ๊ฐ ์์ผ๋ ์คํ ํธ๋ ์ด์ค๋ฅผ ์ซ์ ์๋ ์์๋ค. ์ด๋ฐ "์กฐ์ฉํ ์คํจ"๋ ๋ณดํต ์ฝ๋๊ฐ ์๋๋ผ ํ๊ฒฝ ์ชฝ ์ ์ ๊ฐ ์ด๊ธ๋ฌ์ ๋ ๋์จ๋ค.
์์ธ: ๋ฉ์ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ + React 18 StrictMode
๋๋๊ทธ๋ react-beautiful-dnd๋ก ๋ถ์ฌ ๋จ์๋ค. ๋ ๊ฐ์ง๊ฐ ๊ฒน์ณ ์์๋ค.
์ฒซ์งธ, react-beautiful-dnd๋ ๋ ์ด์ ์ ์ง๋ณด์๋์ง ์๋๋ค. ์์ ์(Atlassian)๊ฐ ๊ฐ๋ฐ์ ์ค๋จํ๊ณ , ์ ์ฅ์ README์๋ ์ ์ง๋ณด์ ์ข
๋ฃ๊ฐ ๋ช
์๋ผ ์๋ค. React 16~17 ์์ ์ ๋ง๋ค์ด์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ผ ์ต์ React์ ๋์ ๋ณํ๋ฅผ ๋ฐ๋ผ๊ฐ์ง ๋ชปํ๋ค.
๋์งธ, React 18์ StrictMode๊ฐ ๊ฐ๋ฐ ๋ชจ๋์์ effect๋ฅผ ๋ ๋ฒ ์คํํ๋ค. React 18๋ถํฐ StrictMode๋ ์ปดํฌ๋ํธ๋ฅผ ๋ง์ดํธ โ ์ธ๋ง์ดํธ โ ๋ค์ ๋ง์ดํธํ๋ฉฐ effect์ ์ ๋ฆฌ(cleanup)์ ์ฌ์คํ์ ๊ฐ์ ๋ก ํ ๋ฒ ๋ ๋๋ฆฐ๋ค. "cleanup์ ์ ๋๋ก ์ ์ง ์ปดํฌ๋ํธ๋ฅผ ๊ฐ๋ฐ ๋จ๊ณ์์ ์ก์๋ด๋ผ"๋ ์๋๋ค.
// TimeTrack์ ์ง์
์ โ StrictMode๋ก ๊ฐ์ธ์ ธ ์์๋ค
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);๋ฌธ์ ๋ react-beautiful-dnd์ <Droppable>์ด ์ด ์ด์ค ๋ง์ดํธ๋ฅผ ๊ฒฌ๋๋๋ก ์ค๊ณ๋์ง ์์๋ค๋ ๊ฒ์ด๋ค. ๋๋กญ ์์ญ์ ๋ง์ดํธ๋ ๋ ๋ด๋ถ ๋ ์ง์คํธ๋ฆฌ์ ์์ ์ ๋ฑ๋กํ๋๋ฐ, StrictMode๊ฐ ์ด ์ปดํฌ๋ํธ๋ฅผ ์ธ๋ง์ดํธํ๋ค๊ฐ ๋ค์ ๋ง์ดํธํ๋ ๊ณผ์ ์์ ๋ฑ๋ก์ด ์ด๊ธ๋๋ค. ๊ฒฐ๊ณผ์ ์ผ๋ก ๋๋กญ ์์ญ์ด ๋ ์ง์คํธ๋ฆฌ์ ์๋ ์ํ๊ฐ ๋๊ณ , ๋๋๊ทธ๋ฅผ ์์ํด๋ "๋์ ๊ณณ"์ ๋ชป ์ฐพ์ ์๋ฌด ๋ฐ์์ด ์๋ค.
์๋ฌ๊ฐ ์ ๋ฌ ์ด์ ๋ ์ฌ๊ธฐ์ ์๋ค. throw๊ฐ ์๋๋ผ ๊ฐ๋ฐ ์ฝ์์ ๊ฒฝ๊ณ (warning) ๋ก๋ง ๋์ค๊ณ (์: Unable to find draggable, droppable ๋ฑ๋ก ๊ด๋ จ invariant ๊ฒฝ๊ณ ), ์ฑ์ ๊ทธ๋๋ก ๋ ๋๋๊ธฐ ๋๋ฌธ์ ํ๋ฉด์์ผ๋ก "๋๋๊ทธ๋ง ์ ๋๋" ๋ฉ์ฉกํ ์ฑ์ฒ๋ผ ๋ณด์ธ๋ค.
์ ๋ฆฌํ๋ฉด, ๋ด ์ฝ๋๊ฐ ํ๋ฆฐ ๊ฒ ์๋๋ผ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์ง๊ธ์ React๋ฅผ ๋ชป ๋ฐ๋ผ๊ฐ ๊ฒ์ด๋ค. StrictMode๋ฅผ ๊บผ์ ์ฆ์์ ๋ฎ์ ์๋ ์์ง๋ง, ๊ทธ๊ฑด ๊ฐ๋ฐ ๋จ๊ณ ์์ ์ฅ์น๋ฅผ ํฌ๊ธฐํ๋ ํํผ์ผ ๋ฟ ์์ธ ํด๊ฒฐ์ด ์๋๋ค.
ํด๊ฒฐ: API๊ฐ ๋๊ฐ์ ํฌํฌ๋ก import๋ง ๊ต์ฒด
react-beautiful-dnd์๋ ์ ๊ด๋ฆฌ๋๋ ์ปค๋ฎค๋ํฐ ํฌํฌ @hello-pangea/dnd ๊ฐ ์๋ค. ์๋ณธ๊ณผ API๊ฐ ์์ ํ ๋์ผํ ๋๋กญ์ธ(drop-in) ๊ต์ฒด์ฉ์ด๋ฉด์, React 18๊ณผ StrictMode ํธํ ๋ฌธ์ ๊ฐ ๊ณ ์ณ์ ธ ์๋ค. ์ฆ ์ปดํฌ๋ํธ ์ฝ๋๋ ํ ๊ธ์๋ ์ ๋ฐ๊พธ๊ณ import ๊ฒฝ๋ก๋ง ๋ฐ๊พธ๋ฉด ๋๋ค.
// โ ๊ฐ๋ฐ ์ค๋จ + React 18 StrictMode์์ Droppable ๋ฏธ๋ฑ๋ก
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
// โ
API ๋์ผํ ์ ์ง๋ณด์ ํฌํฌ โ StrictMode ํธํ
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";package.json์์ ์์กด์ฑ๋ง ๊ฐ์ ๋ผ์ด๋ค. ํฌํฌ์ ๋ฉ์ด์ ๋ฒ์ ์ด ์ง์ React ๋ฒ์ ์ ๋ฐ๋ผ๊ฐ๋ฏ๋ก(18 ๊ณ์ด โ ^18), React ๋ฒ์ ๊ณผ ๊ฒฐ์ ๋ง์ถ๋ค.
// package.json
{
"dependencies": {
// "react-beautiful-dnd": "^13.1.1", โ ์ ๊ฑฐ
"@hello-pangea/dnd": "^18.0.1", // React 18 ๋์
"react": "^18.3.1"
}
}DragDropContext / Droppable / Draggable์ props์ ๋ ๋ ํ๋กญ ์๊ทธ๋์ฒ๊ฐ ๊ทธ๋๋ก๋ผ, ๊ธฐ์กด ๋ชฉ๋ก ๋ ๋๋ง ์ฝ๋๋ ์๋ ํ์๊ฐ ์๋ค.
<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId="tasks">
{(provided) => (
<ul ref={provided.innerRef} {...provided.droppableProps}>
{activeTasks.map((task, index) => (
<Draggable key={task.id} draggableId={task.id} index={index}>
{(provided) => (
<li
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{/* ... */}
</li>
)}
</Draggable>
))}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>ํจ์ ํ๋ ๋: draggableId๋ ๋ฐ๋์ ๋ฌธ์์ด
์๋ณธ๊ณผ ํฌํฌ ๋ชจ๋ draggableId์ droppableId๋ฅผ ๋ฌธ์์ด๋ก๋ง ๋ฐ๋๋ค. ์ซ์ id๋ฅผ ๊ทธ๋๋ก ๋๊ธฐ๋ฉด ์ด๊ฑด ์กฐ์ฉํ ์คํจ๊ฐ ์๋๋ผ ๋ช
์์ ์ผ๋ก ๊นจ์ง๋ค(draggableId must be a string).
TimeTrack์ ํญ๋ชฉ id๋ฅผ ์ฒ์๋ถํฐ ๋ฌธ์์ด๋ก ๋ง๋ค๊ณ ์์ด์ ์ด ํจ์ ์ ํผํ๋ค.
// id๋ฅผ ๋ฌธ์์ด๋ก ์์ฑ โ "t-<base36 ์๊ฐ>-<base36 ์ํ์ค>"
let seq = 0;
export const makeTaskId = () =>
`t-${Date.now().toString(36)}-${(seq++).toString(36)}`;๋ง์ฝ ๋ฐฐ์ด ์ธ๋ฑ์ค๋ ์ซ์ PK๋ฅผ id๋ก ์ฐ๊ณ ์๋ค๋ฉด String(id)๋ก ๊ฐ์ธ์ ๋๊ฒจ์ผ ํ๋ค. ์๊ฐ ๊ธฐ๋ฐ + ์ํ์ค ์กฐํฉ์ด๋ผ ๊ฐ์ ๋ฐ๋ฆฌ์ด์ ์ฌ๋ฌ ๊ฐ๋ฅผ ๋ง๋ค์ด๋ ์ถฉ๋ํ์ง ์๋๋ค(๋๋๊ทธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ id ์ค๋ณต์ ๋ฏผ๊ฐํ๋ค).
๊ฒ์ฆ
import ํ ์ค๊ณผ package.json ์์กด์ฑ์ ๊ต์ฒดํ ๋ค, StrictMode๋ฅผ ์ผ ์ฑ๋ก ๊ฐ๋ฐ ์๋ฒ๋ฅผ ๋ค์ ๋์ ๋ค.
| ํญ๋ชฉ | ๊ต์ฒด ์ (react-beautiful-dnd) |
๊ต์ฒด ํ (@hello-pangea/dnd) |
|---|---|---|
| ๋๋๊ทธ ์ ํญ๋ชฉ ์ด๋ | ๋ฌด๋ฐ์ | ์์ ๋ฐ๋ผ ์ด๋ |
์์ ์ฌ์ ๋ ฌ(onDragEnd) |
๋์ ์ ํจ | ์ ์ ๋ฐ์ |
| ์ฝ์ ๊ฒฝ๊ณ | Droppable ๋ฑ๋ก ๊ด๋ จ ๊ฒฝ๊ณ | ์์ |
| ๊ธฐ์กด ์ปดํฌ๋ํธ ์ฝ๋ ์์ ๋ | โ | 0์ค (import ๊ฒฝ๋ก๋ง) |
StrictMode๋ฅผ ๋์ง ์๊ณ ๋ ๋๋๊ทธ๊ฐ ์ด์๋ฌ๋ค๋ ๊ฒ ํต์ฌ์ด๋ค. ์ฆ์์ ๋ฎ์ ๊ฒ ์๋๋ผ ์์ธ์ ์์ค ๊ฒ์ด๋ค.
์ค๋ฌด ์ฒดํฌ๋ฆฌ์คํธ
DnD๋ฟ ์๋๋ผ, ์๋ฌ ์์ด ํน์ ๊ธฐ๋ฅ๋ง ์กฐ์ฉํ ์ฃฝ๋ ์ํฉ์ ๋ง๋ฌ์ ๋:
- ๋จผ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ ์ง๋ณด์ ์ํ๋ฅผ ํ์ธํ๋ค. GitHub ์ ์ฅ์๊ฐ ์์นด์ด๋ธ๋๊ฑฐ๋ README์ "no longer maintained"๊ฐ ์์ผ๋ฉด ๊ทธ๊ฒ ์ ๋ ฅํ ์์ธ์ด๋ค.
- React 18 ์ด์์ด๋ผ๋ฉด StrictMode์ ์ด์ค ๋ง์ดํธ๋ฅผ ์์ฌํ๋ค. ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ effect๊ฐ ๋ ๋ฒ ๋๋ ๊ฑธ ๋ชป ๊ฒฌ๋๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค.
- StrictMode๋ฅผ ๋๋ ๊ฑด ํํผ๋ค. ์ง๋จ์ฉ์ผ๋ก ์ ๊น ๊บผ์ ์์ธ์ ์ขํ๋ ๊ฑด OK์ง๋ง, ํด๊ฒฐ์ฑ ์ผ๋ก ์ผ์ง ์๋๋ค.
- ๊ต์ฒด ํ๋ณด๋ API ํธํ ํฌํฌ๋ฅผ ์ฐ์ ์ฐพ๋๋ค. ๋ง์ด๊ทธ๋ ์ด์ ๋น์ฉ์ด import ํ ์ค๋ก ๋๋๋ค.
- ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฉ์ด์ ๋ฒ์ ์ React ๋ฉ์ด์ ์ ๋ง์ถ๋ค(React 18 โ ํฌํฌ
^18). - DnDยท๋ฆฌ์คํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ id๋ ๋ฌธ์์ด, ๊ทธ๋ฆฌ๊ณ ์ ์ผํด์ผ ํ๋ค. ์ซ์ PK๋
String()์ผ๋ก ๊ฐ์ผ๋ค.
๋ง๋ฌด๋ฆฌ
์์ ์ธ ํธ์ ๋ฒ๊ทธ๊ฐ ๋ด ์ฝ๋(ํด๋ก์ ยท๋ ๋ยท๋ธ๋ผ์ฐ์ ํ์ด๋จธ) ๋๋ฌธ์ด์๋ค๋ฉด, ์ด๋ฒ์ ์์กด์ฑ์ด ์๊ฐ์ ๋ชป ๋ฐ๋ผ์จ ๊ฒฝ์ฐ์๋ค. ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ํ ๋ฒ ๋ถ์๋ค๊ณ ๋์ด ์๋๋ผ, ๋ฐํ์(์ฌ๊ธฐ์ React 18 StrictMode)์ด ๋ณํ๋ฉด ๊ฐ์ด ํ๋ค๋ฆฐ๋ค. ์๋ฌ ์์ด ๊ธฐ๋ฅ๋ง ์กฐ์ฉํ ์ฌ๋ผ์ก์ ๋ ์ฝ๋๋ถํฐ ํ๊ธฐ ์ ์ "์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์์ง ์ด์ ์๋" ๋ฅผ ๋จผ์ ๋ฌผ์ด๋ณด๋ ํธ์ด ๋น ๋ฅด๋ค.
๊ต์ฒด๋ ์ฝ๋๋ TimeTrack์ client/src/components/TaskList.jsx์์ ๋ณผ ์ ์๋ค. @hello-pangea/dnd import์ DragDropContext/Droppable/Draggable ๊ตฌ์ฑ์ด ๊ทธ๋๋ก ๋ค์ด ์๋ค.
๊ด๋ จ ๊ธ
- 1ํธ ยท React ํ์ด๋จธ ์ฑ์์ stale closure์ ์ธ ๋ฒ ๋นํ ์ด์ผ๊ธฐ
- 2ํธ ยท useCallback ์๋ ํจ์๊ฐ useEffect๋ฅผ ๋งค ๋ ๋๋ง๋ค ์คํ์ํจ ์ด์ผ๊ธฐ
- 3ํธ ยท ๋ฐฑ๊ทธ๋ผ์ด๋ ํญ์์ setInterval ํ์ด๋จธ๊ฐ ๋๋ ค์ง๋ ์ด์ ์ ๋ฒฝ์๊ณ ํด๋ฒ
- React Hooks ์ ๋ฆฌ โ useState๋ถํฐ ์ปค์คํ ํ ๊น์ง
