프로젝트중에 인스타그램, 블로그처럼 해시태그를 구현하는 부분이 있어서 어떻게 구현을 해야하는지 고민하다가 검색끝에 찾아서 라이브러리 없이 해시태그를 구현해보았다ㅎㅎ
해시태그를 등록 시키는 이벤트는 onKeyUp이벤트를 사용해 KeyDown과 KeyUp이벤트 모두 사용자가 어떤 키를 눌렀는지 알 수 있게된다.
const addHashTag = (e) => {
if (isEmptyValue(e.target.value.trim())) {
return setInputHashTag('');
}
const newHashTag = e.target.value;
setHashTags((prevHashTags) => {
return [...new Set([...prevHashTags, newHashTag])];
});
setInputHashTag('');
};
value의 length길이만큼의 기준으로 ture/false를 반환하고 있기 때문에 사용자가 미입력을 한 채 엔터를 누르면 isEmptyValue 한줄이면 충분하지만, 스페이스바가 한칸 이상 들어간 경우엔 false를 리턴 받는 문제가 생긴다.
// src > app > _lib > isEmpyValue.ts
export const isEmptyValue = (value: string | any[]) => {
if (!value.length) {
return true;
}
return false;
};
그렇게 때문에 trim()메서드를 통해 앞 뒤 공백을 제거한 인자를 넣어줘야한다.
중간에 공백이 들어가는 경우 trim()메서드 만으로는 부족하지만, 스페이스바를 눌렀을 때도 엔터와 동일하게 해시태그가 등록되도록 할 것이기 때문에 문제는 없다.
isEmptyValue(e.target.value.trim())
입력값이 아무것도 없는 스페이스바나 공백인 경우 해시태그가 등록되지 않도록 한 후 리턴 시킨 후 현재 입력값을 비워주었다.
if (isEmptyValue(e.target.value.trim())) {
return setInputHashTag('');
}
현재 인풋값에 작성된 value를 newHashTag라는 변수에 저장시키고 setState를 사용해 추가시켜 중복된 해시태그가 등록되지 않도록 Set해주고 있다.
const newHashTag = e.target.value;
setHashTags((prevHashTags) =>
Array.from(new Set([...prevHashTags, newHashTag]))
);
Set은 중복된 값을 허용하지 않는 자료구조로 중복을 방지하고자 할 때 유용하게 사용된다.
Array.from(new Set([...prevHashTags, newHashTag]))를 사용하여 새로운 해시태그를 추가하기 전에 중복을 제거하고, 유일한 값들만 남도록 하고 있다. 이 방식을 사용하면 중복된 단어를 방지할 수 있다.
해시태그 등록할때 스페이스바,엔터를 사용해 등록되게 리턴시켜줬다.
if (!["Enter", "Space", "NumpadEnter"].includes(e.code)) return;
.replace(/[\{\}\[\]\/?.;:|\)*~`!^\-_+<>@\#$%&\\\=\(\'\"]/g, "")
.replace(/[\{\}\[\]\/?.;:|\)*~!^-_+<>@#$%&\=('"]/g, ""): 정규 표현식을 사용하여 특수 문자를 빈 문자열로 대체해 이것은 입력을 정제하고 데이터베이스에 해시태그를 저장하거나 URL에서 사용할 때 문제가 될 수 있는 문자를 제거하기 위한 것이다.
e.currentTarget.value 대신 e.target.value를 사용하는 것은 일반적으로 같은 동작을 하지만 두 가지는 조금 다른 개념을 나타낸다.
이벤트가 발생한 실제 요소를 가리킨다.
만약 이벤트가 중첩된 요소에서 발생했다면, 이벤트가 발생한 가장 하위의 요소가 e.target이 된다.
현재 이벤트 핸들러가 바인딩된 요소를 가리킨다.
중첩된 요소에서 이벤트가 발생해도 항상 현재 이벤트 핸들러가 바인딩된 요소를 가리킨다.
따라서 e.currentTarget.value를 사용하는 것은, 현재 이벤트 핸들러가 바인딩된 요소에서의 값에 접근한다는 의미이다. 여기서는 입력란에 대한 이벤트 핸들러가 input 요소에 바인딩되어 있으므로 e.currentTarget.value와 e.target.value는 동일한 결과를 제공한다.
두 가지를 혼용해서 사용해도 대부분의 경우에는 문제가 발생하지 않습니다. 하지만 이벤트가 중첩된 환경이거나 다른 상황에서 e.currentTarget와 e.target의 차이가 중요해질 수 있습니다. 일반적으로는 e.target.value를 사용해도 무방은 하다.
// src > app > _lib > hashTag.ts
export const isEmptyValue = (value: string | any[]) => {
if (!value.length) {
return true;
}
return false;
};
export const sanitizeHashTag = (tag: string) =>
tag
.trim()
.replace(/[\{\}\[\]\/?.;:|\)*~`!^\-_+<>@\#$%&\\\=\(\'\"]/g, "")
.replace(/,/g, "");
"use client";
import { useState } from "react";
import { isEmptyValue, sanitizeHashTag } from "./_lib/hashTag";
export default function Home() {
const [inputHashTag, setInputHashTag] = useState<string>("");
const [hashTags, setHashTags] = useState<string[]>([]);
const handleHashTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (!["Enter", "Space", "NumpadEnter"].includes(e.code)) return;
let newHashTag = sanitizeHashTag(e.currentTarget.value);
if (!isEmptyValue(newHashTag) && !hashTags.includes(newHashTag)) {
setHashTags((prevHashTags) =>
Array.from(new Set([...prevHashTags, newHashTag]))
);
setInputHashTag("");
}
};
const handleEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (!["Enter", "NumpadEnter"].includes(e.code)) return;
e.preventDefault();
const regExp = /^[a-z|A-Z|가-힣|ㄱ-ㅎ|ㅏ-ㅣ|0-9| \t|]+$/g;
if (!regExp.test(e.currentTarget.value)) setInputHashTag("");
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputHashTag(e.target.value);
};
return (
<div>
<label>해시태그</label>
{hashTags.map((hashTag) => (
<div key={hashTag}>{hashTag}</div>
))}
<input
value={inputHashTag}
onChange={handleChange}
onKeyUp={handleHashTag}
onKeyDown={handleEnterKey}
placeholder="#해시태그를 등록해보세요. (최대 10개)"
/>
</div>
);
}
한페이지에 코드가 길어서 유틸함수인 부분들은 .ts파일로 빼서 유틸함수로 사용해 코드가 조금더 간결해 진 걸 확인할 수 있다😝