본문 바로가기
교육, 대외활동/유데미 프론트엔드 캠프

[유데미x스나이퍼팩토리] 프론트엔드 프로젝트 캠프 - 사전직무교육 2주차 학습 회고

by daami 2024. 11. 25.

🍀 What I Learned this week

📌 이미지 오픈 API를 통해 이미지 정보 조회 사이트 만들기

📌  Next.js와 Supabase를 이용한 TO-DO List 개발

 

이번주는 총 2개의 프로젝트를 진행하면서 React와 Next, 그리고 Typescript에 대해 자세히 배울 수 있었다.

일주일 만에 2개나 프로젝트를 만들어서 어려웠지만 정말 배운 점이 많았다. 

📌  이미지 오픈 API를 통해 이미지 정보 조회 사이트 만들기

 

구현한 기능

🥯 Jotai 라이브러리의 store를 이용하여 API fetch

 

  • Jotai 라이브러리란 ?
    •  중앙집중식 상태관리 라이브러리
    • props에 대한 depth가 너무 깊어지는 걸 막기 위해 사용한다.(데이터 추적 어려움)
    • atom (=state) : 전역에서 쓰이는 상태값
// src/store/index.ts

import { atom } from "jotai";
import axios from "axios";
export const searchValueAtom = atom<string>('korea');
export const pageAtom = atom<number>(1);
export const fetchAPI = async (searchValue: string, page: number) => {
    const API_KEY = 'gcyfsAL2xYOU7tSWNxnPBikSgoeze88F9cdW2zNwjNM';
    const BASE_URL = 'https://api.unsplash.com/search/photos';
    try {
      const res = await axios.get(
        `${BASE_URL}/?query=${searchValue}&page=${page}&per_page=30&client_id=${API_KEY}`
      );
      return res;
      } catch (error) {
      console.error('API 호출 중 오류 발생');
      throw error;
    }
  }; //store에 모듈화시켜서 코드 재사용성 높임

 

이를 Hompage에서, 서버API로부터 이미지 데이터들을 불러올 때 이용했다.

//src/views/HomePage.tsx

/*...*/

import { useAtom } from 'jotai';
import { fetchAPI, pageAtom, searchValueAtom } from '@/store';

function HomePage() {
  const { toast } = useToast();

  const [searchValue] = useAtom(searchValueAtom);
  const [page] = useAtom(pageAtom);
  const [images, setImages] = useState([]);
  const fetchImages = useCallback(async () => {
    try {
      const res = await fetchAPI(searchValue, page);
      
      if (res.status === 200 && res.data) {
        setImages(res.data.results);
        console.log(res.data);
      } else {
        toast({
          variant: 'destructive',
          title: 'API 호출 실패',
          description: 'API 호출을 위한 필수 파라미터 값을 체크해보세요!',
        });
      }  
    } catch(error) {
      console.log(error);
    }
  }, [searchValue, page, toast]); //필요한 의존성들만 주입
  /*const fetchAPI = async () => {
    const API_KEY = 'gcyfsAL2xYOU7tSWNxnPBikSgoeze88F9cdW2zNwjNM';
    const BASE_URL = 'https://api.unsplash.com/search/photos';
@@ -36,11 +60,11 @@ function HomePage() {
    } catch (error) {
      console.log(error);
    }
  };*/

  useEffect(() => {
    fetchImages();
  }, [fetchImages]); //fetchImages가 변경될 때만 실행

  return (
    <div className="page">
    
   /*...*/

 

 

🥯 navigation bar

동적 라우팅을 위해 경로에 ':id'를 지정했다.

//src/App.tsx

/*...*/

<BrowserRouter>
    <Routes>
      <Route path="/" element={<HomePage/>}></Route>
      {/* 동적 라우팅 */}
      <Route path="/search/:id" element={<HomePage />}></Route>
      <Route path="/bookmark" element={<Bookmark/>}></Route>
    </Routes>
    <Toaster/>
    
  /*...*/

 

 

Homepage에 Nav컴포넌트가 추가되어있으므로, 이 컴포넌트의 값에 따라 다른 주소로 접근할 수 있게 된다.

 

Nav컴포넌트에서 useLocation()으로 현재 URL 정보를 가져왔다.

// src/components/common/nav/Nav.tsx

import { useEffect, useState } from 'react';
import { useAtom } from 'jotai';
import navJson from './nav.json';
import { Link, useLocation } from 'react-router-dom';
import styles from './nav.module.scss';
import { searchValueAtom } from '@/store';

interface Nav {
  index: number;
  path: string;
  label: string;
  searchValue: string;
  isActive: boolean;
}

function Nav() {
  const location = useLocation();
  const [searchValue, setSearchValue] = useAtom(searchValueAtom);
  const [navItem, setNavItem] = useState<Nav[]>(navJson);

  useEffect(()=> {
    navItem.forEach((nav: Nav)=> {
      nav.isActive = false;
      
      if(nav.path === location.pathname || location.pathname.includes(nav.path)){
        nav.isActive = true;
        setSearchValue(nav.searchValue);
      }
    });
    setNavItem([...navItem]);
  },[location.pathname])
  
  /* ... */

 

 

🥯 localStorage를 이용해 북마크 데이터 저장

  • localStorage란?
    • 웹 스토리지(web storage)에는 로컬 스토리지(localStorage)와 세션 스토리지(sessionStorage)가 있다.
    • 세션 스토리지는 웹페이지의 세션이 끝날 때 저장된 데이터가 지워지는 반면에, 로컬 스토리지는 웹페이지의 세션이 끝나더라도 데이터가 지워지지 않는다.
    • 다시 말해, 브라우저에서 같은 웹사이트를 여러 탭이나 창에 띄우면, 여러 개의 세션 스토리지에 데이터가 서로 격리되어 저장되며, 각 탭이나 창이 닫힐 때 저장해 둔 데이터도 함께 소멸한다. 반면에, 로컬 스토리지의 경우 여러 탭이나 창 간에 데이터가 서로 공유되며 탭이나 창을 닫아도 데이터는 브라우저에 그대로 남아 있다.

로그인 기능을 구현하지 않았기에, 한 유저에 대한 북마크 저장 목록을 유지하기 위해 로컬 스토리지를 사용했다.

 

트러블 슈팅

localStorage를 이용해서 북마크 데이터를 저장할 때, 처음에 그냥 findIndex썼더니 오류가 났다.

    const addBookmark = (imageData: ImageDataType) => {
        console.log(imageData);
        const getLocalStorage = localStorage.getItem("bookmark");
        let bookmarks: ImageDataType[] = [];
        if (getLocalStorage) {
            try {
                bookmarks = JSON.parse(getLocalStorage); //parsing을 할 땐 try-catch문으로 에러를 방지해주는 것이 좋다. 
            } catch (error) {
                console.error("Error parsing localStorage:", error);
                bookmarks = [];
            }
        }
        if (bookmarks.length === 0) {
            localStorage.setItem("bookmark", JSON.stringify([imageData]));
            toast({
                title: "로컬스토리지에 올바르게 저장되었습니다.",
            });
        } else {
            const imageExists = bookmarks.findIndex((item: ImageDataType) => item.id === imageData.id) > -1;
            if (imageExists) {
                toast({
                    variant: "destructive",
                    title: "로컬스토리지에 해당 데이터가 이미 저장되어 있습니다.",
                });
            } else {
                bookmarks.push(imageData);
                localStorage.setItem("bookmark", JSON.stringify(bookmarks));
                toast({
                    title: "로컬스토리지에 올바르게 저장되었습니다.",
                });
            }
        }
    };

 

=> Uncaught TypeError: bookmarks.findIndex is not a function at addBookmark

 

타입스크립트는 자바스크립트와 달리, 타입을 정확하게 지정해줘야한다! 따라서 Array인 경우 로직을 수행하도록 명확하게 경우를 구분지어주었다.

 const addBookmark = (imageCard: ImageCardType) => {
    console.log(imageCard);

    let bookmarks: ImageCardType[] = [];
    const getLocalStorage = localStorage.getItem('bookmark'); //null || string

    if (getLocalStorage) {
      try {
        const parsedBookmarks = JSON.parse(getLocalStorage);
        if (Array.isArray(parsedBookmarks)) {
          bookmarks = parsedBookmarks;
        } else {
          console.error("Stored data is not an array");
        }
      } catch (error) {
        console.error("Data parsing error: ", error);
      }
    }
    
    const imageExists = bookmarks.findIndex((item: ImageCardType) => item.id === imageCard.id);
  
    if (imageExists > -1) {
      console.log('Data is already saved.');
    } else {
      bookmarks.push(imageCard);
      localStorage.setItem('bookmark', JSON.stringify(bookmarks));
      console.log('Data has been saved.');
    }
  };

 

 

 

소스코드 : https://github.com/mal0070/react-album

 

 

이 프로젝트는 지난주와 이번주에 걸쳐서 진행되었는데, 복습 겸 구현했던 것을 정리해보았다.

이 주에 진행한 TO-DO List 개발은 다음주에 마무리지으면서 정리해보겠다.

 

 

 

 

 

——————————————————————————

본 후기는 [유데미x스나이퍼팩토리] 프론트엔드 프로젝트 캠프 과정(B-log) 리뷰로 작성 되었습니다.