ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [고양이 사진 검색 사이트] 검색 페이지 - autofocus, Loading, api 연결, try & catch, async, await,img title
    React/과제 테스트 2023. 8. 7. 13:21

    프로그래머스 과제 테스트 준비

    프로그래머스는 html,css, javaScript이기 때문에 실질적으로 도움이 될 만한 React로 준비했다. 

    테스팅에 목적이 아닌 학습을 위한 구현자료로써, 디자인은 하지 않았고 기능에 초점을 두었다. 

     

     

    프로그래머스

    코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

    programmers.co.kr

    🐇 검색 페이지

     

     

    🥕 페이지 진입 시 검색창(input)에 focus처리

    ※ autofocus

    autoFocus속성은 일반 HTML에서와 같은 방식으로 작동합니다.

    css에는 input autofocus가 있다. 하지만, 입력이 여러 개인 경우autoFocus한 페이지에autoFocus무시됩니다.

    그래서 useRef와 useEffect를 사용하여 구현했다.

     

    1. input 검색창에 ref를 이용한다.

    블로그 참고(https://developer-talk.tistory.com/129)

     const inputFocus = useRef(null);
    <input
              type="search"
              placeholder="고양이를 검색해주세요"
              ref={inputFocus}
              value={keyword}
              onClick={clearKeyword}
              onChange={displayKeyword}
              onKeyDown={checkEnterKey}
              onFocus={() => setIsVisibleHistory(true)}
              onBlur={() => setIsVisibleHistory(false)}
            />

    2. useEffect 사용하여 페이지 진입 시 포커스

     useEffect(() => {
        inputFocus.current.focus();
      }, []);

     

    🥕 키워드가 입력된 상태에서 input을 클릭 시 기존 키워드 삭제

    input 태그에 클릭 이벤트를 구현하면 된다. 

     

    1.useState를 사용하여 input value의 상태를 관리한다. 

    // input value로 useState를 사용하여 상태관리, 양반향 데이터 바인딩, 재렌더링 최소화를 한다.
      const [keyword, setKeyword] = useState('');

    2. input 태그의 value를 useState로 사용자의 입력 값을 저장한다.

    <input
              type="search"
              placeholder="고양이를 검색해주세요"
              ref={inputFocus}
              value={keyword}
              onClick={clearKeyword}
              onChange={displayKeyword}
            />
     

    3. input 태그 함수 구현한다. 

    // 사용자가 값을 입력하면 보여준다.
      const displayKeyword = (e) => {
        setKeyword(e.target.value);
      };

      //사용자가 input 클릭시 초기화한다.
      const clearKeyword = () => {
        setKeyword('');
      };

    🥕 사용자의 검색 요청

    Get : /cats/search | query ⇒ q=””

    HTTP/1.1 200 OK
    {
      "data": [{
        id: string
        url: string
        name: string
      }]
    }

    ※ API 연결 (async, await, try, catch)

    API fetch 코드를 async , await 문을 이용하여 수정해주세요. 해당 코드들은 에러가 났을 경우를 대비해서 적절히 처리가 되어있어야 합니다.필수 API 의 status code 에 따라 에러 메시지를 분리하여 작성해야 합니다. 아래는 예시입니다.
      const request = async (url: string) => {     try {       const result = await fetch(url);       return resu

     

    1. ~./api.js 함수 생성

     

    2. api endpoint 상수 생성

    3. 공통 함수 생성

    const fetchData = async (url) => {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json();
      } catch (error) {
        console.error('Error while fetching data:', error);
        return [];
      }
    };

    4. 각각의 함수 작성

    export const getRandomCatData = async () => {
      return fetchData(`${API_ENDPOINT}/api/cats/random50`);
    };

    export const getSearchCatData = async (keyword, pages) => {
      return fetchData(
        `${API_ENDPOINT}/api/cats/search?q=${keyword}&page=${pages}`
      );
    };

    export const getDetailCatData = async (id) => {
      return fetchData(`${API_ENDPOINT}/api/cats/${id}`);
    };

    5. 검색 작동 구현

    event.key === 'Enter'을 사용해서 검색 동작 구별했다.

    // 사용자가 고양이를 검색한다.
      const checkEnterKey = async (event) => {
        if (event.key === 'Enter') {
          await searchCats(keyword);
        }
      };
      const searchCats = async (keyword) => {
        setIsLoading(true);
        const catsList = await getSearchCatData(keyword, currentPages);
        console.log(catsList.data);
        setCatsList(catsList.data);

        if (keyword !== '') saveSearchResultToLocalStorage(keyword);

        // 검색어 중복 제거 및 최대 5개까지 저장
        const historySet = new Set([keyword, ...searchHistory]);
        setSearchHistory([...historySet].slice(0, 5));
        setIsLoading(false);
      };
    <input
              type="search"
              placeholder="고양이를 검색해주세요"
              ref={inputFocus}
              value={keyword}
              onClick={clearKeyword}
              onChange={displayKeyword}
              onKeyDown={checkEnterKey}
              onFocus={() => setIsVisibleHistory(true)}
              onBlur={() => setIsVisibleHistory(false)}
            />

     

    🥕 데이터 요청 시 Loading UI 추가 필요

    ※ API 연결 (async, await, try, catch)

     

    fetch는 async await를 활용하여 진행하고 에러가 나는 경우 적절히 처리한다

    status code에 따라 에러메세지를 분리하여 작성한다

     

    1. loading 상태관리

    const [isLoading, setIsLoading] = useState(false);

    2. loading 컴포넌트 제작

    - 이때, user의 theme 파악

    - spinner로 구현

    import React from 'react';
    import styled, { keyframes } from 'styled-components';

    const Loading = ({ isDarkMode }) => {
      console.log(isDarkMode);
      return (
        <S.SpinnerWrapper isDarkMode={isDarkMode}>
          <S.Spinner />
        </S.SpinnerWrapper>
      );
    };

    const spin = keyframes`
      to { transform: rotate(360deg); }
    `;
    const size = 100;

    const S = {
      SpinnerWrapper: styled.div`
        width: 100vw;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        position: absolute;
        z-index: 3;
        background-color: ${({ isDarkMode }) =>
          isDarkMode ? '#0d0d0d' : '#f5f7f7'};
      `,

      Spinner: styled.div`
        width: ${size}px;
        height: ${size}px;
        border-width: ${size / 12}px;
        border-style: solid;
        border-color: #e6f7fa;
        border-top-color: #cff4fa;
        border-radius: 50%;
        animation: ${spin} 0.8s linear infinite;
        box-sizing: border-box;
      `,
    };
    export default Loading;

    3. loading 상태일 때 loading 컴포넌트 리턴

    if (isLoading) return <Loading isDarkMode={isDarkMode} />;

    🥕 검색결과가 없는 경우 유저가 파악 할 수 있도록 UI 처리 필요

    1. fail 컴포넌트 제작

    2. 검색 결과 없을 시 fail 컴포넌트 리턴

     

    {catsList?.length ? (
              <S.ImgContainer>
                {catsList.map((v, i) => (
                  <S.Img
                    loading="lazy"
                    src={v.url}
                    key={i}
                    alt={v.id}
                    title={v.name}
                    onClick={() => displayModal(v.id)}
                  />
                ))}

     

    🥕 최근 5개의 검색 키워드를 SearchInput아래 표시되도록 생성. 해당 키워드를 선택할 시 검색요청 발생

    1. 최근 검색 기록을 useState로 저장

    const [searchHistory, setSearchHistory] = useState([]);

    2. 검색시 history 저장

    - 단, 중복 제거

    const searchCats = async (keyword) => {
        setIsLoading(true);
        const catsList = await getSearchCatData(keyword, currentPages);
        console.log(catsList.data);
        setCatsList(catsList.data);

        if (keyword !== '') saveSearchResultToLocalStorage(keyword);

        // 검색어 중복 제거 및 최대 5개까지 저장
        const historySet = new Set([keyword, ...searchHistory]);
        setSearchHistory([...historySet].slice(0, 5));
        setIsLoading(false);
      };

    3. history 가시성 여부 state로 관리하기

    const [isVisibleHistory, setIsVisibleHistory] = useState(true);

     

    4. input focus / blur 상태 확인하여 history 보여주기

     

    <input
              type="search"
              placeholder="고양이를 검색해주세요"
              ref={inputFocus}
              value={keyword}
              onClick={clearKeyword}
              onChange={displayKeyword}
              onKeyDown={checkEnterKey}
              onFocus={() => setIsVisibleHistory(true)}
              onBlur={() => setIsVisibleHistory(false)}
            />
     
    {searchHistory.length > 0 && isVisibleHistory && (
              <ul>
                {searchHistory.map((keyword, i) => (
                  <li key={i}>{keyword}</li>
                ))}
              </ul>
            )}

    🥕 페이지 새로고침 시 마지막 검색결과 화면 유지

    최근 검색 기록 localStorge에 저장하여, 페이지 진입시 마지막 검색결과 보여준다. 

     

    1. 페이지 진입시 localStorage에서 검색 결과 불러오기 

    useEffect(() => {
        // Local Storage에서 검색 결과를 불러오는 함수
        const loadSearchResultFromLocalStorage = () => {
          const savedResult = localStorage.getItem('lastSearchResult') || '';
          if (savedResult) {
            console.log('A', JSON.parse(savedResult));
            setKeyword(JSON.parse(savedResult));
            searchCats(JSON.parse(savedResult));
          }
        };
        loadSearchResultFromLocalStorage();
       
      }, []);

     

    2. 검색시 localStorage 검색 키워드 저장

    // 검색 결과를 Local Storage에 저장하는 함수
      const saveSearchResultToLocalStorage = (data) => {
        localStorage.setItem('lastSearchResult', JSON.stringify(data));
      };
     
    // 사용자가 고양이를 검색한다.
    const searchCats = async (keyword) => {
        setIsLoading(true);
        const catsList = await getSearchCatData(keyword, currentPages);
        console.log(catsList.data);
        setCatsList(catsList.data);

        if (keyword !== '') saveSearchResultToLocalStorage(keyword);

        // 검색어 중복 제거 및 최대 5개까지 저장
        const historySet = new Set([keyword, ...searchHistory]);
        setSearchHistory([...historySet].slice(0, 5));
        setIsLoading(false);
      };

     

    🥕 SearchInput옆 자유롭게 버튼을 추가하여 /api/cats/random50 을 호출하여 화면에 렌더링.

     

    1. random 버튼 추가 

    <S.Button onClick={randomCats}>random</S.Button>
     
    // S.styled
    Button: styled.button`
        padding: 10px 20px;
        font-size: 16px;
        background-color: #007bff;
        color: #fff;
        border: none;
        border-radius: 5px;
        cursor: pointer;

        &:hover {
          background-color: #0056b3;
        }

        &:focus {
          outline: none;
          box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
        }

        &:active {
          background-color: #0056b3;
          box-shadow: none;
        }
      `,

     

    2. /cats/random api 호출

    //랜덤으로 고양이를 보여준다.
      const randomCats = async () => {
        setIsLoading(true);
        const catsList = await getRandomCatData();
        console.log(catsList.data);
        setCatsList(catsList.data);
        setIsLoading(false);
      };

     

    🥕 Lazy load를 이해하고 이미지가 화면에 보일 시점에 loading되도록 처리

    - html에서 lazy loading을 구현 방법

    *<image loading = 'lazy'> 사용

    - Lazy Loading

    React.lazy는 컴포넌트에 대한 코드분할을 하기위해 리액트에서 제공하는 내장 기능입니다.

    일반적으로 리액트로 만들어진 앱을 빌드하게 되면, 보통 js파일이 index어쩌구 파일 하나로 통합빌드가 이루어집니다.

    그 말인 즉슨, 최초 앱 렌더링시에 당장 보여지지 않는 컴포넌트라 할지라도 모두 미리 불러오기 때문에 최초 로딩 속도를 지연시킬 가능성이 높습니다.

     

    그리고 이런 문제점을 해결하기 위해 React.lazy가 필요하게 되고, lazy는 필요에 따라 컴포넌트를 불러오도록 동작해주기 때문에 당연히 성능적인 향상을 기대할 수 있습니다

     

    0. lazy, suspense 가져온다

    import { useEffect, useRef, useState, Suspense, lazy } from 'react';

    1. 컴포넌트를 변수로 import 한다.

    const Modal = lazy(() => import('./component/Modal'));

    2. suspense로 감싼다.

    <Suspense fallback={<div>Loading...</div>}>
              <Modal
                isOpen={isModalOpen}
                onClose={() => setIsModalOpen(false)}
                id={currentCat}
              />
    </Suspense>

     

     

     

    🥕 검색결과 각 아이템에 고양이 이름을 노출한다.

    img title 추가

    <S.Img
                    loading="lazy"
                    src={v.url}
                    key={i}
                    alt={v.id}
                    title={v.name}
                    onClick={() => displayModal(v.id)}
                  />

     

Designed by Tistory.