라이브러리 없이 React로 해시태그 구현하기 (feat. 버그와의 싸움)

REASON·2022년 12월 12일
8

React

목록 보기
27/27
post-thumbnail

8개월차 코린이의 라이브러리 사용없이 해시태그 직접 구현하기

갑자기 패기롭게 해시태그를 구현하고 싶어졌다.
원래는 라이브러리를 사용하거나, 그냥 투박하게 일반 텍스트를 백엔드에게 보내버릴까 생각도 했었지만..
일단 일반 텍스트로 만든다면 예쁘지도 않을게 분명하고 지금 아니면 언제 내가 해시태그를 직접 구현해볼까 싶어서 해시태그 구현에 대한 조금의 지식도 없지만 도전해보고 싶어졌다.

처음치곤 꽤 그럴싸하게 만들어지는가 싶었는데
입력중인 이전 값이 지워지지 않는 버그를 만났다...! 😱

<input
  value={inputHashTag}
  onChange={changeHashTagInput}
  onKeyUp={addHashTag}
  onKeyDown={keyDownHandler}
  placeholder='#해시태그를 등록해보세요. (최대 10개)'
  className='hashTagInput'
/>

사실 이전에 더 큰 버그가 있어서 고쳤는데 새로운 버그가 탄생했다. ㅠㅠ
새 버그가 생긴 기념🎁 으로 벨로그에 해시태그를 구현하는 과정을 작성하기로 했다.
그렇게... 버그는 버그를 낳고....... 버그야 생일 축하해 🎉🎉🎉🎉🎉 이제 사라져줬으면 해~!

새롭게 발생한 버그 목록

  1. 새로운 글자를 쓰면 이전 글자가 지워지지 않는 현상
  2. 한글 외의 모든 영어, 숫자, 공백 등 작성이 불가한 현상

뇌피셜로 구현하나요? 구글링해서 먼저 찾아보시죠.

작성된 글을 두개 정도 찾아 보긴 했는데 내가 원하는 구현방향이 아닌 자료들을 만나서 잠시 접어두기로 했다. 예를 들면, 리액트에서 DOM을 직접 조작하는 방향으로 구현한다거나... 아무튼 내가 찾는 자료가 아니었다. 구글링이 답이 아니기도 했고.. 그랬으면 라이브러리를 사용하는 게 더 낫지 않을까(?)

본인은 단순히 인풋 박스 안에서 해시태그만 구현하는 것이 아니라 Form 태그 내부에서 인풋을 조작해야 했기 때문에 엔터를 쳤을 때 기본 동작을 막지 않으면 해시태그 등록이 아닌 폼 전송이 되는 문제도 있었다. 조금 더 스스로 고민해보는게 좋을 것 같다고 판단했기 때문에 당장은 뇌피셜로 구현하더라도 스스로 고민해보고 문제를 해결해보고 싶다는 마음이 컸기 때문.

아무튼, 네이버 블로그 해시태그벨로그 해시태그의 HTML을 뜯어보고 눈대중으로 확인 후 내 나름대로 구현해보기로 했다.

form 기본 동작을 막자! 🎈

일단 본인은 form 안에서 해시태그를 input을 사용하고 있기 때문에
엔터를 쳤을 때 폼 전송이 되는 문제가 있었다.

그렇기 때문에 해시태그 인풋 박스 안에서 엔터를 쳤을 때 폼 전송 기본 동작을 막아야했다.

그렇다면, 엔터를 누르는 시점인 keyDown을 기준으로 기본 동작을 막으면 어떨까?

아니 근데 구현하면서 벨로그 쓰고 있는데 벨로그 올릴려고 새로운 코드 써봤다가
앞서 발생한 버그를 없앴다. 아니 이게 무슨일이람.ㅋㅋㅋㅋㅋㅋ
사실 한글만 작성되는 문제도 있어서 어떻게 해결할까 고민하고 있었는데 오랜 고민하기도 전에 이렇게 허무맹랑하게 끝나버리다니.. 😥

아니 일단은 버그 잡혀서 좋아해야하는 것 아닌가?

본인은 이제 독학 8개월차 코린이라 버그를 만났을 때 좋아해야한다고 생각하기 때문에 동의하지 않는다.
오래 걸리더라도 직접 버그를 해결했을 때 그 짜릿함이 코딩하는 동기부여를 주는 경우도 더러 있었기 때문이다. 서론이 길어졌지만 아무튼, 현재 키다운중인 키가 백스페이스엔터가 아닌 경우 기본 동작을 막아야겠다! 라는 생각에 작성했던 코드가 문제가 있었음을 알 수 있었다.

문제의 그 코드

if (e.code !== 'Backspace' || e.code !== 'Enter') return e.preventDefault();

어떨결에 해결한 코드
벨로그 작성하면서 그냥 써본 코든데 해결되버림. (다시 생각해도 너무 허무해서 슬프다.)

const keyDownHandler = (e) => {
    if (e.code !== 'Enter') return;
    e.preventDefault();
};

아무튼 이렇게 고친 코드는 한글, 영어, 숫자 다 작성이 잘 된다.
백스페이스바도 이전에는 한번만 동작했다면 이제 정상적으로 동작한다.

왜 이전 코드에서 문제가 발생했는가?

당연히 코드를 잘못 짰기 때문이겠지만,,

매번 하나의 문제를 작게 해결하려고 하지 않고 여러 가지를 한번에 해결하려고 했던 나의 실수에서 비롯된 버그였다. 무엇을 해결하고 싶었던 건지 알 수 없는 코드를 짜놓고 왜 버그가 났지 이러고 있었으니 당연한 결과였다.

엔터를 눌렀을 때 기본 동작 막기!!!를 하고 싶었던 거였으니 당연히 엔터가 아닌 값은 모두 리턴시키고 엔터인 경우에만 기본동작을 막게끔 코드를 짰어야 했는데 머릿속에서 온갖 기능에 대한 생각들이 짬뽕된 채로 막 구현해서 저런 기괴한 버그가 발생한 것이다.

아무튼 버그의 원인도 어느정도 파악했으니 다음 진행 사항을 확인해보자.

💫 TODO: 사용자가 공백, 미입력시 해시태그 등록 안되게 막기!

해시태그를 등록시키는 이벤트는 onkeyUp 이벤트를 사용하였다.
keyDown 이벤트와 keyUp이벤트 모두 사용자가 어떤 키를 눌렀는지 알 수 있다.

const addHashTag = (e) => {
  if (e.code !== 'Enter') return;

  if (isEmptyValue(e.target.value.trim())) {
    return setInputHashTag('');
  }

  const newHashTag = e.target.value;
  setHashTags((prevHashTags) => {
    return [...new Set([...prevHashTags, newHashTag])];
  });

  setInputHashTag('');
};

우선 키 코드가 엔터가 아닌 경우 먼저 리턴시키도록 했다.
그 다음 자주 사용하지 않을까? 싶어서 유틸함수로 만들어놨던 isEmptyValue를 사용했는데,
이 유틸 함수는 본인이 JSDOC을 사용해보고 싶어서 처음으로 적용해본 유틸함수다.
(JSDOC..!!! 처음 사용해봐서 너무 뿌듯해오. 🥰)

아직은 내 코드가 어디 내놓기 부끄럽지만...! 해당 유틸함수는 다음과 같다.

/**
 * value의 길이가 0이면 true를 반환한다.
 * @function isEmptyValue
 * @param {(string | any[])} value 문자열 또는 배열
 * @returns {boolean} true 또는 false를 반환한다.
 */

const isEmptyValue = (value) => {
  if (!value.length) {
    return true;
  }
  return false;
};

export default isEmptyValue;

보다시피 이 isEmptyValue 유틸 함수는 lenght를 기준으로 t/f를 반환하고 있기 때문에
사용자가 미입력을 한 채 엔터를 누르면 isEmptyValue 유틸함수로만으로도 충분하지만,
스페이스바가 한 칸 이상 들어간 경우엔 false를 리턴받는 문제가 생긴다.

isEmptyValue(e.target.value.trim())

그렇기 때문에 위와 같이 trim() 메서드를 통해 앞 뒤 공백을 제거한 인자를 넣어주었다.
중간에 공백이 들어가는 경우는 trim 메서드 만으로는 부족하지만, 스페이스바를 눌렀을 때에도 엔터와 동일하게 해시태그가 등록되도록 할 것이기 때문에 당장은 상관이 없다.

if (isEmptyValue(e.target.value.trim())) {
  return setInputHashTag('');
}

입력값이 아무것도 없는 스페이스바나 공백인 경우 해시태가 등록되지 않도록 해야하므로
리턴 시킨 후 현재 입력값을 비워주는 코드를 작성해주었다.

그 다음은 사용자가 입력한 인풋 값이 엔터를 눌렀을 때 등록되도록 추가해주어야 한다.

const newHashTag = e.target.value;
  setHashTags((prevHashTags) => {
    return [...new Set([...prevHashTags, newHashTag])];
  });

setInputHashTag('');

위 코드를 살펴보면 현재 인풋값에 작성된 value를 newHashTag 변수에 저장시키고
setState를 사용해서 추가시키고 있다. 중복된 해시태그가 등록되지 않도록 Set을 사용했다.

이제 해시태그가 등록되었을테니 인풋을 비워주는 코드 setInputHashTag로 마무리했다.

아닛, set을 사용해서 중복을 막은 줄 알았는데..

우리의 사용자는 언제 어떤 이상한 값을 입력할지 모른다. 😑
그래서 원래도 이상한 나지만, 이상한 사용자가 되었다고 생각하고 여러가지 값을 입력해보기로 했다.

스페이스바가 그대로 들어간 채로 해시태그가 등록되는 문제가 발생했다.
스페이스바가 동일하게 들어갔으면 추가되지 않았겠지만..!

해결방법은 간단하다.

 const newHashTag = e.target.value.trim();

그냥 사용자가 입력한 값에도 앞뒤 공백을 없애버리면 되기 때문.

테스트 안해봤으면 사실 이 부분도 간과하고 넘어갔을 것 같긴하다.
간단히 해결할 수 있는 문제라서 다행이군.

💫 TODO: 스페이스바, 콤마를 사용했을 때도 해시태그가 등록되도록 코드 추가하기

이제 중간에 스페이스바가 들어가는 부분을 여기서 해결할 것이다.
네이버 블로그 해시태그에서는 스페이스바나 콤마를 입력해도 해시태그로 등록된다. (이때까지만 해도 콤마도 된다고 생각했는데 나중에 다시 확인해보니 콤마는 특수문자라서 불가하다.)

벨로그에서는 스페이스바는 따로 막지 않는 것으로 확인할 수 있었다. 콤마로는 등록이 되지만 공백은 막지 않았다. 공백만 입력하는 것도 가능했다.

본인은 해시태그 등록으로 스페이스바콤마엔터를 사용하기로 했다.
이 욕심이 화를 부를 줄은 이때까지만 해도 몰랐지만.

앞서 작성한 addHashTag 함수의 if문을 수정해주었다.

const addHashTag = (e) => {
    const allowedCommand = ['Comma', 'Enter', 'Space'];
    if (!allowedCommand.includes(e.code)) return;

    if (isEmptyValue(e.target.value.trim())) {
      return setInputHashTag('');
    }

    const newHashTag = e.target.value.trim();
    setHashTags((prevHashTags) => {
      return [...new Set([...prevHashTags, newHashTag])];
    });

    setInputHashTag('');
  };

변경된 부분은 다음과 같다.

const allowedCommand = ['Comma', 'Enter', 'Space'];
if (!allowedCommand.includes(e.code)) return;

허용가능한 키 코드 배열을 만들어 준 후 이 값이 아닌 경우 리턴시키도록 했다.
여기까지 하면 엔터와 스페이스까지는 정상적으로 동작함을 확인할 수 있다.
콤마의 경우 조금 더 수정해야한다.

보이다시피 콤마로도 등록은 되지만 , 까지 함께 등록이 된다는 문제가 있다.
이제 콤마를 없애는 코드도 작성해보자.

const addHashTag = (e) => {
    const allowedCommand = ['Comma', 'Enter', 'Space'];
    if (!allowedCommand.includes(e.code)) return;

    if (isEmptyValue(e.target.value.trim())) {
      return setInputHashTag('');
    }

    let newHashTag = e.target.value.trim();
    if (newHashTag.endsWith(',')) {
      newHashTag = newHashTag.slice(0, newHashTag.length - 1);
    }

    setHashTags((prevHashTags) => {
      return [...new Set([...prevHashTags, newHashTag])];
    });

    setInputHashTag('');
  };

처음에 if(e.code === 'Comma') 로 할까 하다가 위에 배열에도 콤마가 있는데 또 하드코딩해서 쓰고싶지 않기도 했고.. 갑자기 문득 endsWith가 떠올라서 endsWith를 사용해보았다.

let newHashTag = e.target.value.trim();
if (newHashTag.endsWith(',')) {
  newHashTag = newHashTag.slice(0, newHashTag.length - 1);
}

원래 const로 선언했었지만 콤마인 경우 변수 값을 수정해줘야해서 let으로 변경해주었다.
slice를 사용해서 콤마를 제외한 문자열을 다시 저장시켰다.

이제 다음 문제는 콤마만 작성하는 경우 빈 값이 들어가는 문제가 발생한다.

열심히 빈 값 막으면 뭐해! 또 공백만 들어가는데!

내 유틸함수를 한번 더 사용해야겠다.

if (isEmptyValue(newHashTag)) return;

이러면 이제 리턴당해서 빈 값만 들어가는 문제는 해결된다.

하지만 앞서 작성한 코드는 콤마 한개를 기준으로만 리턴되므로 콤마가 2개 이상일 때는 콤마만 등록이된다. 어차피 특수문자를 막을 것이기 때문에 특수문자를 막으면서 콤마까지 처리해버리자.

💫 TODO: 특수문자를 막자.

원래 특수문자 찾는 정규식은 외계어처럼 생겼을 것 같아서 복붙해오려고 했는데
생각해보니 허용되는 문자가 아닌 것만 전부 막는게 더 간결할 것 같다는 생각이 들었다.
영어, 숫자, 한글이 아닌 것만 막아버리면 되잖아? 싶어서 아! 그러면 이건 내가 직접 정규식 써봐야겠다..!

정규식은 어렵다 😥 실패의 과정 일부,,

정규식을 잘 모르는 초보는 대소문자, 숫자, 한글을 모두 포함하는 정규식을 직접 작성해보며 터득하기로 했다.
왜 어떤건 true이고 어떤건 false일까......? 나름대로 이렇게 쓰면 되지 않을까 했는데 아니였다.ㅠㅠ

콘솔로 테스트해보고 있었는데 처음엔 내가 잘못사용하고 있는건가.. 하고 정규식을 수정해보다가
결국 구글링도 해봤는데 true가 나왔다가 false가 나왔다가 그냥 난장판이길래
뭔가 이상함을 감지. vscode로 동작시켜보니 정상적으로 동작했다.
콘솔의 문제인건가... 정규식을 내가 모른다는 생각에 계속 시도해보느라 시간만 날렸다. ㅋㅋㅋ

const keyDownHandler = (e) => {
  if (e.code !== 'Enter' && e.code !== 'NumpadEnter') return;
  e.preventDefault();

  const regExp = /^[a-z|A-Z|가-힣|ㄱ-ㅎ|ㅏ-ㅣ|0-9| \t|]+$/g;
  if (!regExp.test(e.target.value)) {
    setInputHashTag('');
  }
};

아무튼.. 우여곡절 끝에 정규식도 추가해주었다.
겸사겸사 넘버 패드쪽에있는 엔터도 동작하도록 코드를추가해주었다.
텐키리스 키보드가 아닌 경우에 넘버 패드쪽 엔터도 유효한 엔터긴 하니까..ㅋㅋ

악질 사용자가 되면 정말 문제가 끝도 없다..

미친놈이 되어보자.

이렇게 무자비한 속도로 특수문자를 교묘하게 끼워팔기하면 악질 사용자를 이길 수가 없다.
그렇다면 결국 특수문자를 제거하는 정규식을 추가하는 수밖에 없을 것 같다는 결론에 이르렀다.
특수문자 정규식 안 쓰려고 했는데 결국 쓰게 되네요.

특수문자 정규식은 여기에서 복붙해왔습니다.

const addHashTag = (e) => {
    const allowedCommand = ['Comma', 'Enter', 'Space', 'NumpadEnter'];
    if (!allowedCommand.includes(e.code)) return;

    if (isEmptyValue(e.target.value.trim())) {
      return setInputHashTag('');
    }

    let newHashTag = e.target.value.trim();
    const regExp = /[\{\}\[\]\/?.;:|\)*~`!^\-_+<>@\#$%&\\\=\(\'\"]/g;
    if (regExp.test(newHashTag)) {
      newHashTag = newHashTag.replace(regExp, '');
    }
    if (newHashTag.includes(',')) {
      newHashTag = newHashTag.split(',').join('');
    }

    if (isEmptyValue(newHashTag)) return;

    setHashTags((prevHashTags) => {
      return [...new Set([...prevHashTags, newHashTag])];
    });

    setInputHashTag('');
  };

점점 지저분해지는 코드..
그리고 처음에 endsWith 사용한다고 나댔던 부분 결국includes로 바꿨음..ㅠㅠ
악질사용자를 이기려면 ,를 포함하는 경우 강제로 ,를 없애버리는 수밖에.

왜냐하면 콤마도 엔터, 스페이스와 같이 해시태그를 등록하는 키로 사용하고 있기 때문에
특수문자 정규식부분에 빠져있어서 여러번 마구잡이로 작성하는 경우 강제로 없애줘야 하기 때문.

이제 콤마는 아무리 눌러도 괜찮음을 확인했다.
다른 특수문자도 확인해보도록 하자.

굿. 이제 백엔드에 해시태그를 정상적으로 보낼 수 있겠군요.
물론 이모지는 막지 않았기 때문에 이모지도 등록할 수 있긴하다.

또 다른 악질 사용자가 있으면 코드를 수정해야겠지만 일단은.. 마무리.

TO DO

  • 스타일 수정하기
  • 최대 10개까지만 등록하기로 했으므로 10개 이상은 태그를 등록하지 못하도록 막기
  • 해시태그 최대 글자수 제한하기
  • 코드 리팩토링하기
  • 삭제 기능 구현하기(백스페이스, 클릭)

직접 구현한 해시태그 코드


const [inputHashTag, setInputHashTag] = useState('');
const [hashTags, setHashTags] = useState([]);

const addHashTag = (e) => {
  const allowedCommand = ['Comma', 'Enter', 'Space', 'NumpadEnter'];
  if (!allowedCommand.includes(e.code)) return;

  if (isEmptyValue(e.target.value.trim())) {
    return setInputHashTag('');
  }

  let newHashTag = e.target.value.trim();
  const regExp = /[\{\}\[\]\/?.;:|\)*~`!^\-_+<>@\#$%&\\\=\(\'\"]/g;
  if (regExp.test(newHashTag)) {
    newHashTag = newHashTag.replace(regExp, '');
  }
  if (newHashTag.includes(',')) {
    newHashTag = newHashTag.split(',').join('');
  }

  if (isEmptyValue(newHashTag)) return;

  setHashTags((prevHashTags) => {
    return [...new Set([...prevHashTags, newHashTag])];
  });

  setInputHashTag('');
};

const keyDownHandler = (e) => {
  if (e.code !== 'Enter' && e.code !== 'NumpadEnter') return;
  e.preventDefault();

  const regExp = /^[a-z|A-Z|가-힣|ㄱ-ㅎ|ㅏ-ㅣ|0-9| \t|]+$/g;
  if (!regExp.test(e.target.value)) {
    setInputHashTag('');
  }
};

const changeHashTagInput = (e) => {
  setInputHashTag(e.target.value);
};

<HashTageContainer>
  <InputTitle title='해시태그' />
  <div className='hashTags'>
    {hashTags.length > 0 &&
      hashTags.map((hashTag) => {
      return (
        <div key={hashTag} className='tag'>
          {hashTag}
        </div>
      );
    })}

    <input
      value={inputHashTag}
      onChange={changeHashTagInput}
      onKeyUp={addHashTag}
      onKeyDown={keyDownHandler}

      placeholder='#해시태그를 등록해보세요. (최대 10개)'
      className='hashTagInput'
      />
  </div>
</HashTageContainer>

이렇게 모아놓고 보니 코드는 짧네.........

구현을 고민하는 시간과 버그 고치고, 테스팅하고
악질 사용자 잡는 시간이 오래 걸렸던 것 같다. 약 4시간 조금.. 걸려 구현완료!

처음엔 막막했지만.. 특수문자 정규식 가져온 거 말고는 직접 구현했다는 부분에서 너무 뿌듯한 시간이었다.
남은 투두까지 잘 완성해봐야겠다! 이제 휴식시간~~!😊

1개의 댓글

comment-user-thumbnail
2023년 8월 17일

좋은 코드 감사합니다 !
많이 배우고 가요!

답글 달기