
#해시 태그 기능은 게시물을 구현할 때 거의 빠지지 않고 사용되는 기능이라고 할 수 있습니다. 태그를 구현하는 방법에는 여러가지가 있지만, 위의 GIF와 같이 동적으로 태그를 생성하는 디자인이 가장 많이 사용된다고 할 수 있습니다. 티스토리에서도, Velog에서도 이와 같은 방식으로 해시 태그 기능을 제공하고 있습니다.
제가 진행하고 있는 프로젝트에서도 아래의 요구 사항을 충족하는 태그 기능을 구현해야 했습니다.
- #을 입력하고 스페이스바를 누르면 태그를 생성 (최대 5개까지만 입력 가능)
- 생성된 태그를 눌렀을 때 & 입력이 없는 상태에서 Backspace를 눌렀을 때 태그 삭제
- 드래그 앤 드롭으로 순서 변경 가능하도록
샵 기호를 입력하고 스페이스바를 누르면 태그 생성
태그의 목록들을 보여주기 위한 template 코드는 다음과 같이 작성했습니다.
<div class="input-wrapper">
<label class="input-label">태그</label>
<div class="hash-wrapper">
<div
class="hash-item"
v-for="(tag, index) in hashState.hashArr"
:key="index"
>
<p>{{ tag }}</p>
<p class="hash-item-delete">x</p>
</div>
<input
class="input-tag"
type="text"
v-model="hashState.hashtag"
@keyup.space="onKeyUpSpace"
:placeholder="hashState.hashArr.length < 5 ? '#태그 입력 (최대 5개)' : '최대 5개까지 입력'"
:disabled="hashState.hashArr.length >= 5"
/>
</div>
</div>
hashArr은 태그들의 목록, hashtag는 현재 input에 입력된 tag 문자열의 상태에 해당합니다.
v-model로 hashtag 입력값을 바인딩하였고, vue에서 제공하는 @keyup.space 메소드로 스페이스바를 눌렀을 때 태그를 만드는 로직이 포함된 함수를 실행시켰습니다.
#을 입력하고 난 그 뒤의 문자열의 값으로 태그를 생성하고, 해당 문자열을 hashArr 배열에 push하는 간단한 로직입니다.
const hashState = reactive({
hashArr: [],
hashtag: "",
});
const onKeyUpSpace = (e) => {
if (
hashState.hashtag[0] === "#" &&
hashState.hashtag.substring(1).trim() !== ""
) {
hashState.hashArr.push(hashState.hashtag);
hashState.hashtag = "";
}
};
생성된 태그를 눌렀을 때 & 입력이 없는 상태에서 Backspace를 눌렀을 때 태그 삭제
생성된 태그를 눌렀을 때 태그를 삭제
태그 하나를 구성하는 div에 태그를 삭제하는 함수를 정의하고 연결했습니다.
<div
class="hash-item"
v-for="(tag, index) in hashState.hashArr"
@click="removeHashTag(index)"
:key="index"
></div>
선택된 태그의 index를 받아 배열에서 제거합니다.
const removeHashTag = (index) => {
if (index >= 0 && index < hashState.hashArr.length) {
hashState.hashArr.splice(index, 1);
}
};
입력창이 없는 상태에서 Backspace를 누르면 마지막 태그를 삭제
input의 @keyup.delete
이벤트에 삭제 함수를 정의하고 연결했습니다.
vue에서는 backspace 이벤트 핸들러를 단독으로 제공하지는 않지만, backspace와 delete를 눌렀을 때의 처리를 위한 @keyup.delete
이벤트를 제공합니다.
<input
class="input-tag"
type="text"
v-model="hashState.hashtag"
@keyup.space="onKeyUpSpace"
@keyup.delete="onKeyUpBackspace"
:placeholder="hashState.hashArr.length < 5 ? '#태그 입력 (최대 5개)' : '최대 5개까지 입력'"
:disabled="hashState.hashArr.length >= 5"
/>
e.code가 backspace인지 확인하고, 현재의 입력된 태그가 없는 상태에서 backspace를 눌렀을 때 마지막 태그를 제거합니다.
const onKeyUpBackspace = (e) => {
if (e.code === "Backspace" && hashState.hashtag === "") {
if (hashState.hashArr.length > 0) {
hashState.hashArr.pop();
}
}
};

드래그 앤 드롭으로 순서 변경 가능하도록
해당 기능은 추가로 요청이 들어왔던 부분인데, 드래그 앤 드롭으로 인덱스 값의 위치를 감지하는 것이 쉽지 않을 것이라고 느껴졌습니다.
하지만 Javascript에서 제공하는 드래그 앤 드롭(drag and drop) API를 활용하면 매우 간단하게 구현이 가능한 부분이었습니다.
드래그 앤 드롭(drag and drop) API에 관한 내용은 아래 문서들을 참조했습니다.
- https://developer.mozilla.org/ko/docs/Web/API/DataTransfer
- https://www.tcpschool.com/html/html5_api_dragAndDrop
Vue에서 제공하는 Drag Event Handler + Javascript의 drag and drop API로 작성한 코드입니다.
<div
class="hash-item"
v-for="(tag, index) in hashState.hashArr"
:key="index"
@click="removeHashTag(index)"
draggable="true"
@dragstart="onDragStart(index, $event)"
@dragover="onDragOver(index, $event)"
@drop="onDrop(index, $event)"
></div>
event.dataTransfer.setData
메소드로 드래그가 시작된 index의 값을 저장하고,
event.dataTransfer.getData
메소드로 받은 해당 시작 index의 값 + onDrop이 된 index의 값을 가지고 배열의 순서를 변경하는 로직을 적용했습니다.
const onDragStart = (index, event) => {
event.dataTransfer.setData("tagIndex", index);
};
const onDragOver = (index, event) => {
event.preventDefault();
};
const onDrop = (index, event) => {
event.preventDefault();
const draggedIndex = event.dataTransfer.getData("tagIndex");
const draggedTag = hashState.hashArr[draggedIndex];
hashState.hashArr.splice(draggedIndex, 1);
hashState.hashArr.splice(index, 0, draggedTag);
};
처음에는 @dragstart, @drop 에 대한 부분만 작성했는데 작동하지 않았습니다.
그 이유는 @dragover 이벤트에 대한 처리를 따로 해주지 않아서 발생했던 문제입니다.
dragover event는 드래그 중인 아이템이 지나가는 동안 계속해서 발생합니다.
해당 이벤트를 preventDefault()
처리해주지 않으면 브라우저의 기본 동작이 실행되어 드래그 앤 드롭 동작이 원할하게 이루어지지 않습니다.
따라서 해당 관련 함수를 작성해서 드래그 앤 드롭 기능을 성공적으로 구현할 수 있었습니다.
