[TIL] React로 Tag 구현

박은정·2022년 2월 3일
5

TIL

목록 보기
39/70
post-thumbnail

필요한 기능 탐색

태그기능을 구현하기 전, velog의 콘솔창을 통해 작동원리에 대해 살펴봤는데 아래와 같은 구조로 이루어진 것을 확인했습니다.

참고하면 좋을 것같은 블로그

  • 태그들을 감싸는 div
  • 태그를 입력하는 input : enter 키를 누르면 태그 리스트에 추가됨
  • 추가된 태그를 나타내는 div

1️⃣ 태그들을 감싸는 div

내부의 input이 focus되었을 때 border-color 를 변경해주기 위해 :focus-within 가상선택자를 사용했습니다.

2️⃣ Tag를 입력하는 input

input의 값을 tagItem 이라는 state으로 저장하고,
엔터키를 눌렀을 때 아래와 같은 조건에 부합되면 태그들의 데이터를 담은 array인, tagList 를 추가되도록 구현했습니다.

  1. input.value 문자열의 길이가 0이 아니면서

    • input에 아무런 값도 입력하지 않았을 때 tagList에 추가되지 않기 위한 조건입니다
  2. Enter Key를 받았을 때

    • form요소 내부에 만들었다면 onSubmit 이벤트 헨들러를 사용하면 되었을 것 같습니다

3️⃣ 추가된 태그를 나타내는 div

추가된 태그를 나타내는 div 는 형태는 동일하고 내용만 다르기 때문에 tagList array를 map() 메서드를 통해 tagList의 요소 개수만큼 반복해서 만들었습니다.

따라서, tagItem, tagList state를 관리하는 것이 핵심 구현사항이라고 볼 수 있습니다.

결국 Tag에서 중요하게 다룬 기능은 state 관리 였기 때문에 tagList새로운 tagItem을 추가해주는 것은 어렵지 않게 넘겼습니다만,

tagList에서 tagItem을 삭제할 때, 기존에 Vanilla JavaScript로만 구현했던 것만 떠올라 새로운 발상을 하는데 적은 어려움을 느꼈습니다.

🔥 Error Handling Log

{tagList.map((tagItem, index) => {
  return (
    <TagItem key={index}>
      <Text>{tagItem}</Text>
      <Button onClick={deleteTagItem}>X</Button>
    </TagItem>
  )
})}

여기서 {tagItem} 의 데이터 가져오기가 어려웠는데

  1. useRef를 이용해 DOM 접근하려는 시도

이렇듯 동일한 컴포넌트를 반복해서 만들었기 때문에
useRef를 적용시켰더니, 어떤 태그의 X 버튼을 눌러도 맨 마지막 요소만이 선택되었습니다.

  1. e.target.parentElement.firstChild.innerText

더 나은 방법은 아직 찾지 못해서, 직접 event 객체를 통해 DOM에 접근해서 필터를 적용한 array로 대체했습니다.

코드

import React, { useState } from 'react'
import styled from 'styled-components'

import Title from '../components/Title'

const Tag = () => {
  const [tagItem, setTagItem] = useState('')
  const [tagList, setTagList] = useState([])

  const onKeyPress = e => {
    if (e.target.value.length !== 0 && e.key === 'Enter') {
      submitTagItem()
    }
  }

  const submitTagItem = () => {
    let updatedTagList = [...tagList]
    updatedTagList.push(tagItem)
    setTagList(updatedTagList)
    setTagItem('')
  }

  const deleteTagItem = e => {
    const deleteTagItem = e.target.parentElement.firstChild.innerText
    const filteredTagList = tagList.filter(tagItem => tagItem !== deleteTagItem)
    setTagList(filteredTagList)
  }

  return (
    <WholeBox>
      <Title text='Tag' />
      <TagBox>
        {tagList.map((tagItem, index) => {
          return (
            <TagItem key={index}>
              <Text>{tagItem}</Text>
              <Button onClick={deleteTagItem}>X</Button>
            </TagItem>
          )
        })}
        <TagInput
          type='text'
          placeholder='Press enter to add tags'
          tabIndex={2}
          onChange={e => setTagItem(e.target.value)}
          value={tagItem}
          onKeyPress={onKeyPress}
        />
      </TagBox>
    </WholeBox>
  )
}

const WholeBox = styled.div`
  padding: 10px;
  height: 100vh;
`

const TagBox = styled.div`
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  min-height: 50px;
  margin: 10px;
  padding: 0 10px;
  border: 1px solid rgba(0, 0, 0, 0.3);
  border-radius: 10px;

  &:focus-within {
    border-color: tomato;
  }
`

const TagItem = styled.div`
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 5px;
  padding: 5px;
  background-color: tomato;
  border-radius: 5px;
  color: white;
  font-size: 13px;
`

const Text = styled.span``

const Button = styled.button`
  display: flex;
  justify-content: center;
  align-items: center;
  width: 15px;
  height: 15px;
  margin-left: 5px;
  background-color: white;
  border-radius: 50%;
  color: tomato;
`

const TagInput = styled.input`
  display: inline-flex;
  min-width: 150px;
  background: transparent;
  border: none;
  outline: none;
  cursor: text;
`

export default Tag
profile
새로운 것을 도전하고 노력한다

0개의 댓글