안녕하시렵니까?

최근 개인 사이드 프로젝트로 노션같은 프로젝트 관리 서비스를 한번만들어 보고있는데 말입니다.
그 왜 노션쓰다보면 보드 있지않습니까.
그거 만들려고 하는데 말입니다.
하....이거 감도 안오는거 있지않습니까?
명색의 프론트개발자라고 떠들고다니는 양반이 이런거 하나 못해서 말이야..
전 회사에서 너무 물경력으로 있긴했음... 정신차리고 열심히 살아야지...
그래서 깃에 프로젝트 하나 간단한거 파서 연습한번 해봤음다.
뭐 다른 프레임워크 그런거 안쓰고 걍 html/css 로 UI딸깍 했습니다.
#index.html
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>Drag & Drop 연습</title>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<h1>Drag & Drop 연습</h1>
<div class="board-wrapper">
<div class="column" data-column-id="todo">
<div class="column-header">할 일</div>
<div class="card" draggable="true" data-id="1">HTML 구조 만들기</div>
<div class="card" draggable="true" data-id="2">CSS로 스타일링</div>
<div class="card" draggable="true" data-id="3">JS로 드래그 이벤트</div>
</div>
<div class="column" data-column-id="doing">
<div class="column-header">진행 중</div>
<div class="card" draggable="true" data-id="4">Drag & Drop 연습</div>
</div>
</div>
<script src="script/script.js"></script>
</body>
</html>
#style.css
@charset "UTF-8";
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 40px;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f3f4f6;
}
h1 {
margin-bottom: 20px;
font-size: 24px;
}
.board-wrapper {
display: flex;
gap: 16px;
}
.column {
flex: 1;
background: white;
border-radius: 8px;
padding: 12px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
min-height: 200px;
display: flex;
flex-direction: column;
gap: 8px;
}
.column-header {
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
color: #4b5563;
}
.card {
padding: 8px 10px;
background: #e5e7eb;
border-radius: 6px;
cursor: grab;
user-select: none;
transition: transform 0.1s ease, box-shadow 0.1s ease, opacity 0.1s ease;
}
.card:active {
cursor: grabbing;
}
.card.dragging {
opacity: 0.5;
box-shadow: 0 6px 16px rgba(0,0,0,0.15);
transform: scale(1.03);
}
.placeholder {
height: 36px;
border-radius: 6px;
border: 2px dashed #9ca3af;
margin: 2px 0;
}
.column.drag-over {
background: #f9fafb;
outline: 2px dashed #60a5fa;
outline-offset: 2px;
}
div에 draggable=true 옵션을 넣어줘야 드래그가 가능하다~ 이말씀이야
나머지 UI랑 CSS는 알아서 확인해보시고ㅇㅇ
// 모든 카드 선택
const cards = document.querySelectorAll(".card");
// 모든 컬럼 선택
const columns = document.querySelectorAll(".column");
let draggingCard = null;
let placeholder = null;
cards.forEach(card => {
// 드레그 스타트 함수 이벤트 리스너
card.addEventListener("dragstart", (e) => {
// 드래그 중인 카드 변수에 담고
draggingCard = card;
// dom 에 class name 업데이트 class card => card dragging
card.classList.add("dragging");
// 기본 드래그 이미지는 그대로 두고, 데이터만 세팅
// 이 작업은 이동 작업이라고 브라우저에 선언 ,
// "copy" → 복사
// "move" → 이동
// "link" → 링크
e.dataTransfer.effectAllowed = "move";
// 선택된 카드 정보를 짐처럼 들고 다님.
e.dataTransfer.setData(
"text/plain",
JSON.stringify({
id: card.dataset.id,
fromColumn: card.parentElement.dataset.columnId
})
);
// 카드에서 하는건 현재 어떤 카드가 드래드 되어있는지 class name 을 변경하고(css 영향), 해당 카드의 인덱스를 저장.
});
//드래그 이벤트 종료 리스너
card.addEventListener("dragend", () => {
//dragging class에서 제거 class card dragging => card
draggingCard.classList.remove("dragging");
// 드래그 카드 제거.
draggingCard = null;
// placeholder -> 드래그했을때 들어갈 자리에 미리 보여지던 카드 제거
if (placeholder) {
placeholder.remove();
placeholder = null;
}
//
columns.forEach(col => col.classList.remove("drag-over"));
});
});
columns.forEach(column => {
// 드래그 요소가 각 컬럼 위에 위치하면 발생하는 이벤트리스너
column.addEventListener("dragover", (e) => {
e.preventDefault(); // drop 허용
e.dataTransfer.dropEffect = "move";
// column class 에 drag-over 추가
column.classList.add("drag-over");
//현재 마우스 좌표 다음 카드 요소 확인( 현재 컬럼 , y 좌표)
const afterElement = getDragAfterElement(column, e.clientY);
if (!placeholder) {
placeholder = document.createElement("div");
placeholder.classList.add("placeholder");
}
//뒤에 카드가 더 이상 없을때
if (afterElement == null) {
// 맨뒤에 placeholder
column.appendChild(placeholder);
} else {
// 아니면 afterElement 앞에 넣기
column.insertBefore(placeholder, afterElement);
}
});
// 컬럼 밖으로 드래그가 나갔을 때
column.addEventListener("dragleave", (e) => {
// 같은 걸런 내에서 이동 시 무시
if (e.currentTarget.contains(e.relatedTarget)) return;
// column class 에 drag-over 제거
column.classList.remove("drag-over");
//placeholder 가 있고, placeholder의 부모와 컬럼이 같으면 제거
if (placeholder && placeholder.parentElement === column) {
placeholder.remove();
}
});
//드래그 끝났을때 리스너 추가
column.addEventListener("drop", (e) => {
e.preventDefault();
column.classList.remove("drag-over");
if (!draggingCard) return;
// placeholder 위치에 카드 삽입
if (placeholder && placeholder.parentElement === column) {
column.insertBefore(draggingCard, placeholder);
placeholder.remove();
placeholder = null;
} else {
column.appendChild(draggingCard);
}
const raw = e.dataTransfer.getData("text/plain");
if (raw) {
const data = JSON.parse(raw);
console.log("드롭 데이터:", data);
console.log("toColumn:", column.dataset.columnId);
}
});
});
// 마우스 위치 기준으로 어느 카드 뒤에 들어갈지 계산
function getDragAfterElement(container/* 컬럼 */, y /*y좌표*/ ) {
// 카드에서 dragging 중인 요소 제외한 카드 리스트
const draggableElements = [...container.querySelectorAll(".card:not(.dragging)")];
return draggableElements.reduce(
(closest/* 지금까지 선택된 요소 */, child) => {
const box = child.getBoundingClientRect();
// offset = 현재 내 커서의 y좌표 - 카드의 top 좌표 - box.height / 2 => 카드의 중앙 위치
const offset = y - box.top - box.height / 2;
// 마우스가 이전 카드의 중아보다 위에있고 이전의 찾은 카드보다 더 가까우면
if (offset < 0 && offset > closest.offset) {
// 지금 찾은 카드를 리턴
return { offset, element: child };
} else {
// 이전에 찾은 카드 그대로 리턴
return closest;
}
},
{ offset: Number.NEGATIVE_INFINITY, element: null } // 초기값은 설정 안함.
).element;
}
스크립트 부분입니다.
자, 하나하나 뜯고맛보고 즐겨봅시다
const cards = document.querySelectorAll(".card");
const columns = document.querySelectorAll(".column");
card 랑 컬럼 리스트 가져오는 코드입니다.
뭐 react나 next 에서 사용한다면 state로 관리하는 부분.
card.addEventListener("dragstart", (e) => {
draggingCard = card;
card.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData(
"text/plain",
JSON.stringify({
id: card.dataset.id,
fromColumn: card.parentElement.dataset.columnId
})
);
});
dragstart 는 문자 그대로 드래그가 시작될때 동작하는 함수입니다~
드래그중인 카드를 변수로 담고,
class name 에 dragging 추가
effectAllowed = "move"는 “이 드래그는 이동(move) 작업만 허용해”라고 드래그 시작 쪽에서 선언하는
(일종의 DOM생태계 관리)
setData를 통해 선택된 카드정보를 짐처럼 들고 다님.
아니 그럼 draggingCard 는왜 담은거임??
=> draggingCard는 드래그 중인 DOM 요소 자체를 다른 이벤트 리스너(drop, dragend)에서 직접 조작하기 위해 전역 변수로 들고 있는 것.
column.addEventListener("dragover", (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
column.classList.add("drag-over");
const afterElement = getDragAfterElement(column, e.clientY);
if (!placeholder) {
placeholder = document.createElement("div");
placeholder.classList.add("placeholder");
}
if (afterElement == null) {
column.appendChild(placeholder);
} else {
column.insertBefore(placeholder, afterElement);
}
});
dragover - 해당 요소위에서 드래그 할 때 발생하는 이벤트 리스너.
dropEffect = "move"는 “여기다 놓으면 이동이 일어날 거야”라고 드롭 위치에서 선언하는 것.
const afterElement = getDragAfterElement(column, e.clientY);
현재 마우스 위치 기준으로,
그보다 위에 있는 카드들 중에서 가장 가까운 카드 하나를 찾아서,
그 앞에 placeholder를 넣는 함수
column.addEventListener("dragleave", (e) => {
if (e.currentTarget.contains(e.relatedTarget)) return;
column.classList.remove("drag-over");
if (placeholder && placeholder.parentElement === column) {
placeholder.remove();
}
});
dragleave는 해당 요소에서 드래그가 나갔을때 발생하는 것으로 기존에 담겨있던 클래스를 지워주고.
placeholder가 있다면 해당 요소를 제거해준다.
column.addEventListener("drop", (e) => {
e.preventDefault();
column.classList.remove("drag-over");
if (!draggingCard) return;
// placeholder 위치에 카드 삽입
if (placeholder && placeholder.parentElement === column) {
column.insertBefore(draggingCard, placeholder);
placeholder.remove();
placeholder = null;
} else {
column.appendChild(draggingCard);
}
const raw = e.dataTransfer.getData("text/plain");
if (raw) {
const data = JSON.parse(raw);
console.log("드롭 데이터:", data);
console.log("toColumn:", column.dataset.columnId);
}
});
마지막으로 drop 드래그가 끝날때 발생하는 이벤트 리스너.
드래그 중인 요소가 없다면 그냥 반환.
placeholder && placeholder.parentElement === column
placeholder 가 있고 그거의 부모 컬럼이 지금 컬럼과 같다면 insertBefore로 드래그 중인 카드요소를 placeholder자리에 넣어줌
그게 아니라면 그래그중인 요소를 node에 추가
전체적인 Drag & Drop 흐름
1. dragstart – 드래그 시작: 어떤 요소가 드래그 중인지 기록, 데이터 세팅
2. dragover – 드래그 중: e.preventDefault() 해야 drop 가능, 위치 계산
3. dragleave – 영역 밖으로 나갈 때: 스타일 / placeholder 정리
4. drop – 실제로 놓았을 때: DOM 재배치, 필요한 데이터 처리
5. dragend – 드래그가 완전히 끝난 뒤 정리: 클래스 제거, 변수 초기화
이렇게 한번 드래그요소들 이용해서 연습해보고
진행중인 프로젝트에서 gpt가 만들어준코드 다 지워버리고 다시 코딩해서 원하는 로직대로 동작하도록 완성.
드래드앤드롭 연습 git : https://github.com/Choi-jae-min/dragAndDropEvent
GitHub - Choi-jae-min/dragAndDropEvent: 프론트 드래그 앤 드롭 연습
프론트 드래그 앤 드롭 연습. Contribute to Choi-jae-min/dragAndDropEvent development by creating an account on GitHub.
github.com
사이드 프로젝트 git : https://github.com/Choi-jae-min/TaskGround
GitHub - Choi-jae-min/TaskGround: 팀이 이슈/작업을 생성·배정·추적할 수 있는 웹앱
팀이 이슈/작업을 생성·배정·추적할 수 있는 웹앱. Contribute to Choi-jae-min/TaskGround development by creating an account on GitHub.
github.com
