[WANTED] ASSIGNMENT_5

jun gwon·2022년 11월 12일
0

원티드 프리온보딩

목록 보기
10/14

ASSIGNMENT_5

다섯번째 과제의 요구사항

과제의 대략적인 요구사항은 아래와 같았습니다.

  1. 대략적인 Page 디자인이 주어졌고, 그것에 따라 UI 디자인
  2. 인풋창에 값 입력시 리턴 값들을 List로 보여줄것
  3. API요청을 매 입력시마다 하지 않도록 할것
  4. 캐싱 기능이 있는 Lib를 사용하지 않고, 같은 URL로 API 요청시, 실제 요청은 보내지 않고, 캐싱된 값을 보여줄것
  5. List에 나타난 값들중 인풋값과 같은 글자는 Bold 처리 할것
  6. List값들은 키보드 이벤트로 상호작용 할 수 있도록 할것

++ API 서버는 별도로 주어지지 않으며, json 파일만을 가진 repo를 json 서버로 구동하여 로컬환경에서 작업할것

시작하기에 앞서

초기 셋팅

초기 셋팅은 이전 과제물들과 동일하게 진행하였습니다.
불필요한 파일이 제거된 CRA를 생성하고,
ESLint,Prettier,husky가 설정이 된 repo를 main으로 올린 뒤, 팀원들이 모두 클론하여 각자의 브런치를 만들고 진행하였습니다.

추가적으로, 저는 이번 프로젝트에서 TS를 사용하였기 때문에, 우선 CRA를 클론한 후, typescript CRA를 따로 생성하여, 기존 파일에 덮어 쓰기하여 파일을 변경하여 git을 관리하였습니다.

코딩구현

API 요출 횟수 제한 및 캐싱기능

debounce와 캐시 스토리지를 사용하였습니다.

debounce는 seTimeout을 통해서 일정횟수에 한번씩만 동작하게 하는 함수입니다.
캐시 스토리지는 로컬스토리지와 비슷하게 작동하지만, URL을 파라미터로 받아, 만약 같은 URL로의 요청일시 저장된 캐싱값을 사용하는 브라우저의 내장 기능-함수입니다.

useDebounce 참고 사이트
캐시 스토리지 참고사이트

두 사이트를 통해 얻게된 지식으로 기능구현은 크게 문제 없었습니다.

 const debouncedSearchTerm = useDebounce(inputValue, 300);
  useEffect(() => {
    const getSick = async () => {
      if (isBlankVal(debouncedSearchTerm)) {
        return;
      }
      const SearchURL = `URL...`;
      const cacheData = await getSingleCacheData(debouncedSearchTerm.trim(), SearchURL);
      if (cacheData.length > 0) {
        setSickData(cacheData);
      } else {
        const response = await SearchService.getSick(debouncedSearchTerm.trim());
        setSickData(response.data);
        addDataIntoCache(debouncedSearchTerm.trim(), SearchURL, response.data);
        console.info('calling api');
      }
    };
    if (debouncedSearchTerm) {
      getSick();
    } else {
      setSickData([]);
    }
  }, [SearchService, debouncedSearchTerm]);
  1. useState를 통해 form의 인풋값인 inputValue 를 관리하였고,이것을 useDebounce를 통해 이 값이 0.3초에 한번씩만 리턴되도록 하였습니다.
  2. service는 변경되지 않는 값이며, input값이 변경될시 debounce값도 변경되기에, 인풋값이 변경될때만 해당 useEffect로직이 실행됩니다.
  3. 전체적으로 공백값일시 검색되지 않도록 햇으며, trim을 사용해 앞에 공백이 있어도 작동하도록 하였습니다.
  4. 디바운스 값이 있을시에만 로직이 실행되었으며, 만약 ''과 같은 falsy값일시 빈 배열을 리턴하도록 하였습니다.
  5. 실제 요청 이전에 캐시값이 있으면 요청값 대신 기존 저장된 값을 데이터로 사용하도록 하였습니다.

검색값과 같은 List결과 Bold처리

replaceALl을 사용하여 검색값과 같은 부분은 모두 strong 태그로 감싼 텍스트를 생성하였고, 이후 이 값을 react JSX의 dangerouslySetInnerHTML 기능을 사용하여 텍스트가 태그가 있으면 그대로 반영되도록 하였습니다.

하지만 dangerouslySetInnerHTML의 사용은 지양하는것이 좋기 때문에, regex를 사용하여 배열을 만들고, 그 배열을 map을 돌려 처리하는것이 더 옳은 방법이였다고 생각합니다.

export function parseTextBold(originText: string, targetText: string) {
  const text = originText.replaceAll(targetText, '<strong>' + targetText + '</strong>');
  return text;
}

   <div dangerouslySetInnerHTML={{ __html: parseTextBold(sickName, inputValue) }}></div>

=> 구현과 사용

Form에서 키보드 이벤트를 통해 드랍다운 상호작용

Input이 Focus 되어있는 상황에서, 키보드 이벤트를 통해 렌더링된 List에 UI상의 변화를 주는 요구사항이였습니다.

기능구현은 커스텀훅을 사용하여, Form을 감싸는 container에서 키보드 이벤트를 주었습니다.

대략적인 기능 구상은 아래와 같습니다.

  • input창에서 키보드 이벤트 실행
  • 키 타입이 ArrowUp 또는 down이면 selectNum이라는 state를 통해 몇번째 List가 활성화 되어야 하는지 정한다.
  • 맨끝 또는, 맨위에서 한칸 더 이동하는 경우에 대한 예외처리 정리(state값이 처음 또는 맨 아래로 향하게 한다)

추가적으로, 해당 리스트 값을 클릭시 마찬가지로 Num값에 대한 state를 변경해주도록 하였습니다.
이러한 경우 Div가 클릭되면 input창으로부터 focus가 벗어나게되어 keyboard 이벤트가 발생되지 않는데, forward Ref를 통해 ref를 넘겨주어도 괜찮은 방법이지만, List를 감싸주는 컴포넌트에 hidden input을 만들어 그곳에 focus를 주는 방법을 택하였습니다.

  const onKeyEventHanler = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.nativeEvent.isComposing) {
      return;
    }
    const typing = e.code;
    upDownValHandle(typing, sickData);
  };
  
  export function useSelectVal() {
  const [selected, setSelected] = useState('');
  const [selectNum, setSelectNum] = useState(0);
  const handleSelected = (value: string, idx?: number) => {
    setSelected(value);
    if (idx !== undefined) {
      setSelectNum(idx + 1);
    }
  };

  const upDownValHandle = (typing: string, sickData: sick[]) => {
    switch (typing) {
      case 'ArrowDown':
        if (sickData.length <= selectNum) {
          setSelectNum(1);
          const selectData = sickData[0];
          handleSelected(selectData?.sickNm);
        } else {
          setSelectNum(prev => prev+1);
          const selectData = sickData[selectNum];
          handleSelected(selectData?.sickNm);
        }
        break;
      case 'ArrowUp':
        if (selectNum <= 1) {
          setSelectNum(sickData.length);
          const selectData = sickData[sickData.length - 1];
          handleSelected(selectData?.sickNm);
        } else {
          setSelectNum(prev => prev-1);
          const selectData = sickData[selectNum - 2];
          handleSelected(selectData?.sickNm);
        }
        break;
      default:
        break;
    }
  };

==> selected는 선택된 리스트이 텍스트값입니다.
==> const selectData = sickData[selectNum - 2];
와 같은 조건의 경우, 최초에 ArrowDown이 발동하고, up을 할 경우는 예외 조건에 걸리기때문에 up에서의 else문이 발동되지 않습니다. 때문에 최소 2번이상의 ArrowDown 함수가 발동 이후에 up함수의 else부분이 발동되기때문에 -2를 통해 이전 값을 바라보게 됩니다.

이러한 방식으로 구현을 하게되었는데,

구현을 해가며, 한가지 예상치 못한 버그를 발견하였습니다.
인풋값이 영어일때는 로직이 예상대로 실행이 되었지만, 한글이 들어온경우에는 연속하여 2번 실행되는 버그였습니다.

찾아낸 문제에 대한 설명 및 해결법은 아래와 같습니다.
input에서 한글과 같은 조합식 문자를 사용하는경우, OS에서는 composition 이라는 과정을 거치게 됩니다. OS에서는 영어가 아닌 글자가 들어오면, 이것을 해당 언어 글자로 바꿔주는 이벤트를 처리하게됩니다. 이러한 과정에서 키보드 이벤트가 발생하면 OS와 브라우저 동시에 이벤트가 실행되기 때문에, 영어로 입력시에는 정상적으로 작동하지만, 한글의 경우에는 중복실행이 되는것이였습니다.

이 문제는 자바스크립트의 nativeEvent인 isComposing 옵션을 통해, 만약 컴포징중일시에는(OS와 중복 실행될 가능성이 있는 동안에는) 함수가 실행되지 않도록 처리하였습니다.

 const onKeyEventHanler = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.nativeEvent.isComposing) {
      return;
    }
    const typing = e.code;
    upDownValHandle(typing, sickData);
  };

==> 키보드 이벤트에서 isCopsing 중이면 함수가 실행되지 않도록 하였습니다.

좋았던 부분 및 개선된 부분

  1. UI를 짜는 부분은 많이 발전한것 같습니다.
  2. 이전 service를 이용하는 모델은 처음 사용하는 것이기도하였고, class를 사용하는게 낯설엇는데, 이번 프로젝트에선 꽤나 개선된 모습을 보였고, 불필요한 로직을 줄였고, 로직을 잘 분리했던것 같습니다.
  3. 마찬가지로 커스텀훅은 익숙친 않은 방식이였는데, 이를 잘 활용하여 키보드 이벤트를 구현한것 같습니다.
  4. TS를 공부환경이 아닌, 과제물에서 직접 사용해보는건 처음이였는데, 생각보다 수월하게 진행할수있어 좋았습니다.
  5. 디바운스 기능과 캐쉬 스토리지 사용등, 처음 사용하는것들을 알아보고 잘 사용할수 있었던것 같아 좋았습니다.
  6. 키보드 이벤트를 이용한 무한 리스트 이동형식은 주어진 요구사항은 아니였지만, 구현하게 되어 뿌듯했습니다.
  7. 마찬가지로 요구 사항 이외의 기능인, 저장된 캐시의 유효기간 이후 삭제 하는 방법 등을 생각하고 구현한 점 등이 좋았습니다.
  8. 인풋값 -> 키보드 이벤트 발생시 예상치 못한 버그를 스스로 문제를 찾고 해결하게 되어 좋았습니다. (iscomposing 문제)
  9. 팀원 중 한분이 json 서버를 vercel에 배포하는 방법을 알려주셨고, 그곳의 API를 사용해 로컬환경이 아닌곳에서도 프로젝트를 배포하여 사용할수있게 되어 좋았고, json서버를 배포하는 방법을 알게되어 좋았습니다.

아쉬웠던 부분

  1. 캐시 스토리지를 사용하는데, 이기능을 잘 분리시키지 못하였고, context 단에서 사용하려다보니, 결과적으로 URL을 하드코딩한 느낌이 있어 아쉬웠습니다. 이번 같은경우 service단에서 처리하는것이 더 좋앗을것이라 생각했습니다.
  2. URL요청에 대해서 매 단어마다 체크를 하다보니, 매 단어를 가진 캐쉬스토리지가 오픈되었습니다. 이러한 것보다는 차라리 공통된 URL에서만 캐쉬스토리지를 만들고 사용하면 더 좋았을것이라 생각했습니다.
  3. 키보드 이벤트를 통한 리스트 결과 액티브 상태 이동을 원형 형식으로 무한하게 이동 가능하게 구현하였는데, 이 구현방법을 이해를 바탕으로한 구현보다는, 감에 의존한 구현을 하게 된것 같아 아쉬웠습니다.
  4. 커스텀 훅을 통하여 키보드 이벤트를 구현하였는데, 커스텀훅은 전역값이 아니다 보니, 하위 컴포넌트에서 동일한 props를 필요로 하다면 여러번 전달해주어야 해서, hooks를 사용하기보단 context를 사용하여 구현하는것이 더 옳은 방법이였던것 같습니다.
  5. tailwind-styled-compoents Lib를 사용하고잇는데, 현재 TS와는 호환이 잘 안되는 이슈가 있습니다. 때문에 모든 컴포넌트에 any타입을 지정해주어야 하는데, 시간적 여유가 됏다면 twin.macro 등으로 마이그레이션 해보고 싶었는데 못한게 아쉬웠습니다.
  6. 장점이기도 하지만, 단점이기도 한 예상치 못한 버그를 찾는데 시간이 너무 오래 걸렸던것 같습니다. iscomposing는 정말 생각치 못한 이슈였기 때문에, 정확하게 문제를 찾는데 시간이 오래 걸렸던것 같습니다.
    버그가 발생하였을때, 우선적으로 어떠한곳에서 문제가 발생하는지 범위를 점점 좁혀나가고, 명확하게 이슈를 체크하는것이 중요한것 같다 생각했습니다.
  7. 완성해서 되돌아보니, 이번 프로젝트에서 로직을 압축할수 있었고, 이벤트 처리에 대한 이해도 조금 부족하였던것 같습니다.

배포 링크

(작업 기간 약 2일)
https://pre-onboarding-7th-3-1-9-june.vercel.app/

Git Repo

https://github.com/jun-05/pre-onboarding-7th-3-1-9

BestPractice 선정 결과

캐싱 기능을 깔끔히 구현하셨고, 기타 요구사항이외의 것들을 모두 작성하신 분의 과제물이 BestPractice 로 선정되었습니다.

진행 중 아쉬웠던 점

  • 원해서 한 팀장역은 아니였지만, 팀원 분들과의 정확한 의사소통이 잘 안되는것 같아 아쉬웠습니다.
  • 코스가 중반을 넘어섰고, 여러모로 피로한 코스여서 그런지, 팀원분들이 지친것 같은 모습이 보여 아쉬웠습니다.

진행 중 좋았던 점

  • 과제물의 요구사항에서 API요청을 줄이는것과, 캐싱 기능 구현에 대한 정보에 대한 의견이 빠르게 나왔고, 그에 대한 정보공유 및 사이트 공유가 잘 이루어져 좋았습니다.
  • 캐싱 기능 구현 방법, 텍스트 bold 처리 방법 등, 그것에 대한 코드공유가 이루어져서 같은 문제여도 역시 여러가지 방법이 있다는것을 알게되어 좋았습니다.

0개의 댓글