Skip to content

pre-onboarding-frontend-7-team-3/pre-onboarding-7th-3-1-3

Repository files navigation

원티드 프리온보딩 프론트엔드 3팀 - Assignment #5

한국임상정보 홈페이지 검색창 및 검색어 추천 구축

프로젝트 기간 : 2022년 11월 8일 ~ 2022년 11월 11일


📖 목차


⌨️ 실행 방법

$ git clone https://github.com/pre-onboarding-frontend-7-team-3/pre-onboarding-7th-3-1-3.git
$ npm install
$ chmod ug+x .husky/*
  • API 서버 실행
$ npm run serve-json
  • 별도 터미널에서 Client 실행
$ npm start

📃 협업 과정

  1. 비동기적 소통을 위해 노션 워크스페이스에서 프로젝트를 페이지와 컴포넌트로 나누고 미팅 로그와 주요 코드를 공유하여 개발 효율을 높이고자 노력했습니다.

    노션 링크

  2. 본 프로젝트는 동료학습에 최적화된 과정을 찾아가며 진행했습니다. VSC Live Code extension을 활용하여 라이브 코드 리뷰를 진행하고 각자 구현한 코드에 대한 피드백을 진행하여 Best Practice를 추가해 나가는 과정을 거쳤습니다. 후의 리팩토링도 동일한 과정을 거쳐 진행하였습니다.

  3. 소통 플랫폼으로 게더타운과 디스코드를 활용해서 협업을 진행했습니다.


☑️ Best Practice 및 채택 근거

1. TypeScript

  • TypeScript는 정적 타입을 지원하므로 컴파일 단계에서 오류를 포착할 수 있는 장점이 있습니다. 코드의 가독성을 높이고 예측할 수 있게 하며 디버깅이 쉽다는 장점에 모두 공감해서 채택했습니다. 명시적인 정적 타입 지정은 팀 단위로 협업 시에 의도를 명확하게 코드로 기술할 수 있다는 점에서도 의견을 모았습니다.

2. API 호출 최적화

  • 서버에 대한 API 호출 최적화를 위해 응답으로 받은 데이터는 캐싱 처리하고 비동기 호출 횟수의 단축을 위해 디바운싱 처리했습니다.

  • 2-1. API 호출별로 로컬 캐싱

    • 클라이언트에서 디바운싱 시간을 이탈하여 API 호출을 이룰 때마다, 트라이(Trie) 자료구조 형식으로 캐시 스토리지에 데이터를 저장합니다.

    • 후에 클라이언트에서 다시 API 호출을 하기 전, 서버로 보낼 쿼리 스트링을 캐시 스토리지에 저장된 캐시 object key를 비교하여 일치하는 캐시 데이터의 필요 부분을 추출하여 관련 검색어를 출력하였고, 일치하지 않을 때(캐시 처리된 데이터가 없는 경우) 서버에 다시 API 요청을 보내는 방식으로 구현했습니다.

    ezgif com-gif-maker (2)

    const isValidInput = validateText(searchInputValue) && searchInputValue;
    const handleSearch = async () => {
    const TrieWordList = makeTrieBySearchWord(searchInputValue);
    const cachedData = await getCachedData(TrieWordList);
    const isCachedData = validateText(searchInputValue) && cachedData;
    try {
    if (isCachedData) {
    const JsonCachedData = await cachedData.json();
    setDiseaseListData(filterCachedData(JsonCachedData, searchInputValue));
    }
    if (!isCachedData) {
    const JsonApiData = await getDataAndRegisterCache(searchInputValue);
    setDiseaseListData(JsonApiData);
    }
    } catch (err: unknown) {
    if (err instanceof AxiosError) {
    throw err;
    }
    }
    };


  • 2-2. API 호출 횟수 최적화

    • 검색창에 검색어를 입력했을 때 onChange 이벤트가 발생할 때마다 서버에 GET 요청을 보내는 것은 비효율적인 프로세스라고 공통된 의견을 나누었습니다.

    • 따라서 첫 onChange 이벤트의 발생 시점으로부터 의도적인 지연시간을 두어 API 호출 횟수를 줄였습니다.

    • 검색창의 onChange 이벤트가 비동기적으로 input의 상태 값을 업데이트하되, 사용자가 입력한 검색 결과에 대한 비동기 요청은 디바운싱 함수에서 설정한 시간(600ms)이 지난 뒤에 최종적으로 업데이트된 상태 값을 쿼리 스트링으로 보내 호출되게 구현했습니다.

    3-1 디바운싱 후

    3-1 디바운싱 전

    import { useState, useEffect } from "react";
    const useDebounce = (value: string, delay = 600) => {
    const [debounceValue, setDebounceValue] = useState(value);
    useEffect(() => {
    const timer = setTimeout(() => {
    setDebounceValue(value);
    }, delay);
    return () => {
    clearTimeout(timer);
    };
    }, [value, delay]);
    return { debounceValue };
    };
    export default useDebounce;


3. 키보드만으로 추천 검색어들로 이동 가능한 UX 구축

  • 사용자가 추천 검색어 간 키 이벤트(ArrowUp, ArrowDown)로 자유롭게 이동할 수 있게 구현했습니다.

  • 검색창 이동 간 선택된 검색어는 하이라이트 처리로 UI를 구성했고 검색 목록 하단에 도달했을 때 ArrowUp, ArrowDown 이벤트에는 자동으로 목록 내 검색어 위치로 따라가도록 구현했습니다.

  • 일반적인 사용자 검색 유형을 고려하여 페이지를 다시 돌아오거나 새로고침 했을 시에도 최근 검색어가 유지되도록 구현하였습니다. onKeyDown 이벤트가 발생했을 때 호출되는 함수는 커스텀 훅으로 분리하여 뷰 단에서의 로직을 최소화하고자 노력했습니다.

    3-1 스크롤

    import { useCallback, useMemo } from "react";
    import { useRecoilState, useRecoilValue } from "recoil";
    import { searchResultState } from "store/searchResult";
    import { selectedSearchResultIndex } from "store/selectedSearchResultIndex";
    const useKeyDown = () => {
    const [selectedIndex, setSelectedIndex] = useRecoilState(selectedSearchResultIndex);
    const diseaseListData = useRecoilValue(searchResultState);
    const keyData = useMemo(() => {
    return {
    currentDataLength: diseaseListData.length,
    isEmptyData: diseaseListData.length === 0,
    isFirstItem: selectedIndex <= 0,
    isLastItem: selectedIndex === diseaseListData.length - 1,
    };
    }, [diseaseListData, selectedIndex]);
    const onArrowDown = () => {
    keyData.isLastItem ? setSelectedIndex(-1) : setSelectedIndex((prev) => prev + 1);
    };
    const onArrowUp = () => {
    keyData.isFirstItem
    ? setSelectedIndex(keyData.currentDataLength - 1)
    : setSelectedIndex((prev) => prev - 1);
    };
    const onKeyDown = useCallback(
    (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (["ArrowDown", "ArrowUp"].includes(e.key)) {
    e.preventDefault();
    }
    const isSingleWord = e.nativeEvent.isComposing;
    const isSearchResultEmpty = keyData.isEmptyData;
    if (isSingleWord || isSearchResultEmpty) return;
    switch (e.key) {
    case "ArrowDown":
    onArrowDown();
    break;
    case "ArrowUp":
    onArrowUp();
    break;
    default:
    break;
    }
    },
    [selectedIndex, diseaseListData]
    );
    return onKeyDown;
    };
    export default useKeyDown;


4. 객체지향형 프로그래밍

  • live share로 코드 리뷰 중 재사용성이 높고 독립적으로 반복하는 코드에 대한 리팩토링이 필요하다는 의견을 공유했습니다.

  • 컴포넌트(모듈, 유틸리티 함수, React 컴포넌트 등) 간 직접적인 의존성을 낮추고, 둘 다 공통된 추상화에 의존해야 한다는 의존성 역전 원칙을 적용하고자 API 통신 함수에 적용했습니다.

  • 다만 이러한 원칙을 코드 전체적으로 적용하는 것은 악영향을 주거나 오버 엔지니어링된 코드로 이어질 수 있다는 공통된 의견을 모았습니다.

interface SearchDiseaseService {
get(url: string): Promise<any>;
search(keyword: string): Promise<any>;
}
export class SearchDiseaseServiceImp implements SearchDiseaseService {
private httpClient: any;
constructor(httpClient: any) {
this.httpClient = httpClient;
}
get client() {
return this.httpClient;
}
async get(url = "") {
const res = await this.client.request({ method: "get", url });
return res;
}
async search(keyword: string) {
const res = await this.client.request({ method: "get", url: `/sick?sickNm_like=${keyword}` });
return res;
}
}

🔒 팀 코드 컨벤션

  • git commit message
커밋명 내용
feat 파일, 폴더, 새로운 기능 추가
fix 버그 수정
docs 제품 코드 수정 없음
style 코드 형식, 정렬, 주석 등의 변경
refactor 코드 리팩토링
test 테스트 코드 추가
chore 환경설정, 빌드 업무, 패키지 매니저 설정등..
hotfix 치명적이거나 급한 버그 수정
remove 사용하지 않는 변수, 파일 etc 삭제
working 이미 만들어진 기능, 함수 작업중
merge branch merge
  • branch
브랜치명 내용
develop 파일, 폴더, 새로운 기능 추가
fix 버그 수정
docs 제품 코드 수정 없음
refactor 코드 리팩토링
hotfix 치명적이거나 급한 버그 수정
feat 새로운 기능 추가

🔨 사용 기술

HTML5 CSS3 JavaScript React TypeScript

styled-components recoil

Git GitHub Notion


📦 폴더 구조

📂 src
│  ├─ App.tsx
│  ├─ apis
│  │  ├─ SearchDiseaseService.ts // axios를 통한 기본적인 API 호출
│  │  ├─ getDataAndRegisterCache.ts // fetch를 통해 cache storage에 저장
│  │  ├─ index.ts
│  │  └─ request.ts // axios baseurl
│  ├─ components
│  │  ├─ Header.tsx
│  │  ├─ Layout
│  │  │  ├─ Layout.tsx
│  │  │  └─ index.ts
│  │  ├─ Main
│  │  │  ├─ HighlightedText.tsx
│  │  │  ├─ RecentSearchWord.tsx
│  │  │  ├─ RecommendWord.tsx
│  │  │  ├─ SearchForm.tsx
│  │  │  ├─ SearchItem.tsx
│  │  │  └─ SearchItemList.tsx
│  │  └─ Navbar.tsx
│  ├─ constants
│  │  └─ NavData.ts
│  ├─ hooks
│  │  ├─ useDebounce.ts // 검색에 대한 디바운스 처리 (useSearch에 종속적)
│  │  ├─ useKeyDown.ts // 검색어 방항키 이동에 대해 'item index'를 전역화
│  │  ├─ useScroll.ts // 전역 item index를 받아 뷰 단을 구현
│  │  └─ useSearch.ts // '검색중인 내용' 전역화
│  ├─ index.css
│  ├─ index.jsx
│  ├─ pages
│  │  └─ Main
│  │     ├─ Main.tsx
│  │     └─ index.ts
│  ├─ react-app-env.d.ts
│  ├─ store
│  │  ├─ searchResult.ts
│  │  ├─ searchValue.ts
│  │  ├─ searchWord.ts
│  │  └─ selectedSearchResultIndex.ts
│  ├─ style
│  │  ├─ GlobalStyle.ts
│  │  └─ Theme.ts
│  ├─ styled.d.ts
│  └─ utils
│     ├─ checkValidationOfInput.ts // 검색 값의 유효성(단일 자음, 단일 모음 false) 판단
│     ├─ filterCachedData.ts // 캐시 데이터 필요 부분(현재 검색된 단어와 일치되는 부분 까지만) 추출
│     ├─ getCachedData.ts // 캐시 데이터에 검색 중인 값이 존재하는지 확인, 존재하면 가져오기
│     ├─ makeTrieBySearchWord.ts // 검색된 단어로 자료구조(Trie) 생성
│     └─ recentSearch.ts // 최근 검색어 파싱
└─ tsconfig.json


👨‍👩‍👧‍👦 팀원

고영훈
(팀장)
조은지
(팀원)
김창희
(팀원)
박정민
(팀원)
YeonghunKO Joeunji0119 PiperChang ono212
YeonghunKO Joeunji0119 PiperChang ono212
문지원
(팀원)
이상민
(공지)
이지원
(팀원)
조수진
(팀원)
moonkorea00 dltkdals224 365supprot suzz-in
moonkorea00 dltkdals224 365support suzz-in