본문 바로가기
✨FRONTEND/📍React

React 태그 입력 기능 구현하기

by 짱돌보리 2025. 3. 23.
728x90

React 태그 입력 기능 구현하기⌨️

구현 목표

- 프로젝트를 등록할 때, 기술 스택을 태그 형태로 관리하는 기능을 추가하고 싶음!!(like 티스토리 태그 기능, 벨로그 태그 기능)
- 엔터 시 태그 등록태그 삭제
- 태그 개수 제한

1. 상태(state) 정의

const [stacks, setStacks] = useState<string[]>([])
const [stackTag, setStackTag] = useState('')
  • 최종적으로 반환될 stacks
  • stackTag는 현재 사용자가 입력하고 있는 개별 태그 값

2. Enter 키 입력 시 태그 등록 (onKeyDown)

const handleTagInput = (e: ChangeEvent<HTMLInputElement>) => {
  setStackTag(e.target.value)
}

const inputKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
  if (e.key === 'Enter' && stackTag) {
    if (!stacks.includes(stackTag)) {
      setStacks([...stacks, stackTag])
    }
    setStackTag('')
    e.preventDefault()
  }
}

 

사용자가 Enter 키를 눌렀을 때 실행
stackTag가 비어있지 않고, 이미 존재하지 않는 태그일 경우에만 추가하기!

 

추가 후에는 stackTag를 비워서 입력창을 초기화하고 e.preventDefault()로 Enter의 기본 동작(폼 제출)을 막아준다!

3. 태그 삭제 기능

const removeTag = (removeTagIndex: number) => {
  const newStacks = stacks.filter((_, index) => index !== removeTagIndex)
  setStacks(newStacks)
}

 

각 태그 옆에 삭제 아이콘(X 버튼)을 만들어 삭제 함수를 넣어줬다.
filter()를 사용해 클릭한 태그의 index를 제외한 나머지 태그만 남긴다!

<div className="flex flex-col gap-2">
          <label className="text-xl font-semibold">⚒️ 기술 스택</label>
          <input
            type="text"
            value={stackTag}
            onChange={handleTagInput}
            onKeyDown={inputKeyDown}
            placeholder="태그를 입력하세요 (엔터를 누르면 태그가 적용돼요, 최대 8개)"
            className="text-custom-white focus:border-custom-white border-custom-gray-200 rounded-lg border-2 bg-transparent px-4 py-3 outline-none"
          />
          <div className="flex flex-wrap gap-2">
            {stacks.map((stack, index) => (
              <div
                key={index}
                className="bg-primary text-custom-white flex items-center gap-1 rounded-full px-4 py-2"
              >
                {stack}
                <Image
                  src={remove}
                  onClick={() => removeTag(index)}
                  className="h-5 w-5 cursor-pointer"
                  alt="Remove tag"
                />
              </div>
            ))}
          </div>
          {stacks.length > 8 && (
            <p className="text-custom-red">
              기술 스택은 최대 8개까지 입력할 수 있습니다
            </p>
          )}
          {errors.techStack && (
            <p className="text-custom-red">{errors.techStack.message}</p>
          )}
        </div>

 

 

구현 완!!!

 


🚨 zod, react-hook-form required 오류

 

이제 폼을 제출할 때 stacks 배열을 react-hook-form에 전달하려고 했는데, 이상한 오류가 발생했다... 바로 required 오류... 에러메세지에서 스키마에 명시한 오류가 아닌 required 만 떴다.

 

알고보니 react-hook-form은 기본적으로 input과 textarea에서 value를 string으로 관리하는데, stacks는 zod에서 array(z.string())로 정의되어 있었다. 이때 react-hook-form은 input에서 string 값을 기대하는데, stacks는 배열이기 때문에 타입이 맞지 않아서 required 오류가 발생한 거였뜸..!

import { z } from 'zod'

export type ProjectInput = z.infer<typeof projectSchema>

export const projectSchema = z.object({
  title: z
    .string()
    .min(1, '제목은 필수입니다')
    .max(50, '제목은 최대 50자 이하이어야 합니다'),
  description: z.string().min(1, '본문은 필수입니다'),
  techStack: z.array(z.string()).min(1, '기술 스택을 1개 이상 입력해주세요'),
  deadline: z.string().min(1, '마감일을 선택해주세요'),
})

해결방법)) setValue로 react-hook-form에 수동으로 값 넣기

stacks 배열을 react-hook-form의 techStack 필드에 수동으로 넣어주자.

 

폼 제출 직전에 setValue를 사용해서 stacks 배열을 react-hook-form에 전달하면 오류가 해결된다!!

  // stacks 배열이 바뀔 때마다 react-hook-form에 주입
  useEffect(() => {
    setValue('techStack', stacks)
  }, [stacks, setValue])

 

해결 후 오류 메세지가 잘 뜬돠 🥳🥳🥳