๐Ÿ“˜ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ์ด ์กฐ์šฉํžˆ ์ฃฝ์—ˆ๋‹ค โ€” react-beautiful-dnd์™€ React 18 StrictMode

@leekh8 ยท July 14, 2026 ยท 5 min read

React ๋ฒ„๊ทธ ํ•ด๋ถ€ ์‹œ๋ฆฌ์ฆˆ


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 ๊ฒฝ๊ณ ), ์•ฑ์€ ๊ทธ๋Œ€๋กœ ๋ Œ๋”๋˜๊ธฐ ๋•Œ๋ฌธ์— ํ™”๋ฉด์ƒ์œผ๋ก  "๋“œ๋ž˜๊ทธ๋งŒ ์•ˆ ๋˜๋Š”" ๋ฉ€์ฉกํ•œ ์•ฑ์ฒ˜๋Ÿผ ๋ณด์ธ๋‹ค.

DnD ๋‚ด๋ถ€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌDroppable(๋“œ๋กญ ์˜์—ญ)React 18 StrictMode (dev)DnD ๋‚ด๋ถ€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌDroppable(๋“œ๋กญ ์˜์—ญ)React 18 StrictMode (dev)์žฌ๋“ฑ๋ก์ด ์–ด๊ธ‹๋‚จ โ€” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ด์ค‘ ๋งˆ์šดํŠธ๋ฅผ ๋ชป ๊ฒฌ๋”ค๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ๋“œ๋กญ ์˜์—ญ์ด ์—†์Œ โ†’ ๋“œ๋ž˜๊ทธํ•ด๋„ ๋†“์„ ๊ณณ์„ ๋ชป ์ฐพ์Œ(๋ฌด๋ฐ˜์‘)1) ๋งˆ์šดํŠธ๋“œ๋กญ ์˜์—ญ ๋“ฑ๋ก2) ์–ธ๋งˆ์šดํŠธ (StrictMode ๊ฐ•์ œ cleanup)๋“ฑ๋ก ํ•ด์ œ3) ๋‹ค์‹œ ๋งˆ์šดํŠธ

์ •๋ฆฌํ•˜๋ฉด, ๋‚ด ์ฝ”๋“œ๊ฐ€ ํ‹€๋ฆฐ ๊ฒŒ ์•„๋‹ˆ๋ผ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ง€๊ธˆ์˜ 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 ๊ตฌ์„ฑ์ด ๊ทธ๋Œ€๋กœ ๋“ค์–ด ์žˆ๋‹ค.


๊ด€๋ จ ๊ธ€


@leekh8
๋ณด์•ˆ, ์›น ๊ฐœ๋ฐœ, Python์„ ๋‹ค๋ฃจ๋Š” ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ