Git Product home page Git Product logo

pokemon's Introduction

고객 만족 💐 대박 리뷰 이벤트 💐 고객 감동

Hits

Hello! 👋

🏕 habitat

habitat url
📧 Email [email protected]
🥕 Youtube https://www.youtube.com/@dusunax
🌱 Blog/tech https://velog.io/@dusunax
🌾 Blog/diary https://dusunax.tistory.com/
👩‍🌾 Notion https://dusunax.notion.site

🪬 Tech Stack

category skills
Front-end 🐔 JavaScript TypeScript React Next.js Tanstack Query Redux Zustand StyledComponent Tailwind Three.js
Back-end 🐤 Node.js Nest.js MongoDB
Others Github, Docker, AWS, Agile Methodologies

⌛️ 2024's Learning Goals

  • 📚 Conducting a deep dive into my current tech stack to enhance my computer science knowledge, aiming to become a long-term contributing developer.
    • Processing: Modern Javascript Deep Dive(2023), Modern React Deep Dive(2024.3~), Typescript
  • 📝 Building a portfolio/blog by 2024 to showcase TILs (Today I Learned) and project examples etc.
  • 🧩 Researching and gaining insights into monorepos, design systems, and web views.
  • 🤖 build a AI related projects.

GitHub stats

dusunax's GitHub stats

pokemon's People

Contributors

dusunax avatar

Stargazers

 avatar

Watchers

 avatar

Forkers

valentine256

pokemon's Issues

Tailwind CSS 작업 약간 _ 230224

Tailwind CSS 작업 약간 _ 230224

  • 23년 2월 24일, 저녁 10시 40분~12시
  • [tailwind] CSS 작업
  • 아이템 갯수 수정
  • #28

image


🗂 작업내용

[tailwind] CSS 작업 약간

아이템 갯수 수정


✨ [tailwind] CSS 작업 약간 ✨

영역 남는 공간에 그라디언트 추가

<div className="w-full md:max-w-md flex-1 sm:rounded-lg flex flex-col justify-between drop-shadow-xl scroll-p-2 overflow-y-scroll  scrollbar-hide overflow-x-hidden scroll bg-gradient-to-b from-red via-[#244952] to-light-blue">

다음과 같이 disaplay: none ⇒ block은 안됨(none인 요소에 hover 할 수 없음)

hidden hover:block

따라서 opacity로 수정한 코드

{/* 포켓몬 넘버, 포켓몬 이름 */}
<div className="name px-2 rounded-lg absolute -top-2 left-1/2 -translate-x-1/2 text-xxs break-keep bg-zinc-500 text-white opacity-0 hover:opacity-100">
  <div className={isCachedRange ? " text-green-300" : ""}>
    {(no > 0 ? no : "") + " " + names["ko"]}
  </div>
</div>

image

src\styles\index.css에 다음 hover 코드 추가

.new-pokemon:hover .name {
  opacity: 1;
}

클릭 가능한 포켓몬에 border 추가

<div
  className={`new-pokemon px-2 pt-4 my-2 rounded-full mx-auto bg-[#ebf3f3] grad shadow-inner-custom relative ${
    isCachedRange && " border-2 border-red"
  }`}
>

✨ 아이템 갯수 수정 ✨

  • 아이템 갯수 24개로 수정 후, 반응형 col 아이템 갯수 수정
  • 4 => 6 => 4 => 3
<ul className="grid grid-cols-3 xxs:grid-cols-4 sm:grid-cols-6 md:grid-cols-4 gap-2">

image

데이터 패칭 및 unactive 쿼리 키 코드 개선 _ 230210

데이터 패칭 및 unactive 쿼리 키 코드 개선 _ 230210

23년 2월 10일, 저녁 10시~11시15분


데이터 패칭 & 로딩 & 저장 관련 코드 개선

시점 데이터 패칭(api) 로딩(view) DB에 저장(firestore)
다음 포켓몬을 프리패칭 O X X
포켓몬을 화면에 출력 X O X
포켓몬이 화면에서 사라짐 - - O

수정 내용

  1. 기본 쿼리키 변경
  • id 기본값이 1이면 이상해씨만 계속 나오고, 뽑기로 이상해씨가 나온 경우와 기본값을 구분할 수 없으므로, 기본값을 0으로 변경합니다.
  1. 예외 추가 (id가 0일 때)
  • 0일 때 예외처리 할 곳 : 요청 내에서 얼리리턴하면 반환값이 꼬이므로 0일 때 api 요청 보내지 않습니다.
  • useQuery에서 기본 fallback과 함께 얼리리턴도 사용
const fallback = initPokemon();
const { data: newPokemon = fallback } = useQuery(
  [queryKeys.pokemon, idNo.curr],
  () => {
    if (idNo.curr === 0) return fallback;

    return getPokemonQuery(idNo.curr);
  },
  {
    retry: 2,
  }
);
  1. inactive값 지우기
  • update할 때 queryClient.clear()로 unactive값(가비지 값) 지우기
    image
  1. unactive값이 지워질 때 DB에 값 저장
  • 새 포켓몬을 가져오고, 이전 포켓몬이 화면에서 사라질 때
    (즉, 프리패칭한 쿼리 키 데이터를 로딩하고, 현재 쿼리 키 데이터를 clear할 때)
    • 현재 포켓몬을 DB에 저장합니다.
function updateIdNo(): void {
  setIdNo({ curr: idNo.next, next: getId() });
  queryClient.clear();
  // console.log(newPokemon);
  dbService.collection("pokemonDB").add(newPokemon);
}
  • 원하는 시점에 저장되는 부분 확인
    • 187 에레키드
      image
    • 187 에레키드가 사라질 때, DB에 저장
      image

image가 안보입니다

query 내부의 image url로 직접 접속하면 포켓몬 이미지가 보이는데 사이트 내에서는 안보입니다.
아마 next function 요청에 문제가 있는 게 아닐까 싶네요
제 파쪼옥, 거북손데스 보여주세요..
image

Firestore & React-query : 리스트 페이지네이션 추가 _ 230214

Firestore & React-query : 리스트 페이지네이션 추가 _ 230215

  • 23년 2월 14일, 저녁 10시 30분~12시 30분
  • 포켓몬 리스트에 no순으로 정렬한 페이지네이션을 추가했습니다.
  • #14

image


🗂 작업내용

1. [firebase, react-query] 포켓몬 리스트에 페이지네이션 추가

  • limit와 page를 사용합니다.
  • Firestore에서 no순으로 정렬된 페이지네이션 리스트를 가져옵니다.
  • React-query에 no순으로 정렬된 페이지네이션 리스트를 저장하고 화면에 출력합니다.

2. [UI] 페이지네이션 버튼 추가


✨ [firebase, react-query] 포켓몬 리스트에 페이지네이션 추가 ✨

유저의 포켓몬 리스트 정렬

  • 다음과 같이 페이지네이션이 적용된 리스트를 가져옵니다.
    • limit는 3, page는 0에서부터 시작
    • src\api\pokemonAPI.ts
    • firebase & data fetching : API에서 작업
/** 유저의 포켓몬 리스트를 가져옵니다. */
export const fetchPokemonDB = async (limit: number, next: number) => {
  const { user } = await getFirestoreRefObject();
  const list = user.data().pokemonList.sort((a: PokemonDTO, b: PokemonDTO) => {
    return a.no - b.no;
  });

  // limit is 3
  // page starts at 0
  // 0, 2
  // 3, 5
  // 6, 8

  return list.slice(next * limit, next * limit + limit);
};

페이지네이션 useQuery입니다.

  • 이전 쿼리키를 교체합니다.
    • src\components\pokemon\hooks\usePokemonQuery.ts
    • useQuery & cache 관련 : 커스텀 훅에서 작업
const { data: pokemonList = [] } = useQuery(
  [queryKeys.pokemonList, limit, page],
  async () => {
    const list = await fetchPokemonList(limit, page);
    queryClient.setQueryData([queryKeys.pokemonList, idNo.curr], list);
    return list;
  },
  {
    staleTime: 60000,
    refetchOnWindowFocus: false,
    retry: 2,
  }
);
  • 이전 코드입니다.
const { data: pokemonList = [] } = useQuery(
    [queryKeys.pokemonList, idNo.curr],
    async () => {
      const list = await fetchPokemonList();
      queryClient.setQueryData([queryKeys.pokemonList, idNo.curr], list);
      return list;
    },
    {
      keepPreviousData: true,
    }
  );

totalPages를 가져옵니다.

  • limit을 다르게 입력하면 바뀐 값 리턴
    • src\api\pokemonAPI.ts
      • UI와 API 요청 제한을 위한 값입니다.
/** 페이지네이션 관련 데이터 */
export const setPaginationFromUserRef = async (limit: number) => {
  const { user } = await getFirestoreRefObject();

  const totalPokemonNumber = user.data().totalPokemonNumber;
  const totalPages = Math.ceil(totalPokemonNumber / limit);

  return { totalPages };
};

포켓몬 리스트입니다.

  • src\components\pokemon\pokemon-list\PokemonList.tsx
    • 버튼에 totalPages를 사용합니다.
const [totalPages, setTotalPages] = useState<null | number>();

useEffect(() => {
  const setTotalPage = async () => {
    const { totalPages } = await setPaginationFromUserRef(limit);
    setTotalPages(totalPages);
  };
  setTotalPage();
}, [limit]);

✨ [UI] 페이지네이션 버튼 추가 ✨

UI

  • 버튼 추가
<button
  className={page === 0 ? "text-gray-300" : ""}
  onClick={() => page > 0 && setPage(page - 1)}
></button>
<button
  className={page === totalPages - 1 ? "text-gray-300" : ""}
  onClick={() => page < totalPages - 1 && setPage(page + 1)}
>
  
</button>

image

Firebase : timestamp를 사용한 타이머 추가 _ 230215

firebase timestamp를 사용한 타이머 추가

  • 23년 2월 14일, 저녁 10시 30분~12시 30분
  • firebase timestamp를 사용한 타이머를 추가합니다.
  • #16

image


🗂 작업내용

[firebase] 타이머 추가

  • timestamp를 사용합니다.
  • 현재 시간과, 마지막 포켓몬을 뽑은 시간의 차이를 구합니다.

[UI] 버튼 비활성화

  • 제한 시간이 지나지 않았다면 버튼을 비활성화 합니다.

✨ [firebase] 타이머 추가 ✨

  • DB에서 사용하는 시간 new Date()로 통일

firebase에서 lastDrawTime 가져오기

  • src\components\timer\hooks\useTimer.ts
const [lastTime, setLastTime] = useState("");
const [timeGap, setTimeGap] = useState("");
const [isOverHour, setIsOverHour] = useState(false);

useEffect(() => {
  async function initUserObject() {
    const { userRef } = await getFirestoreRefObject();
    const userData = (await userRef.get()).docs[0].data();

    const gap = await getTimeGap();
    setTimeGap(formatTimeGap(gap));
    setLastTime(formatTimestamp(userData.lastDrawTime));
    setIsOverHour(gap >= oneHour);
  }

  initUserObject();
}, [oneHour]);

시간 formatter

  • timeGap을 문자열로 변환
    • padStart()로 0 붙일 수 있음
    • src\utils\timeFormatter.ts
function formatTimeGap(timeGap: number) {
  const hours = Math.floor(timeGap / (60 * 60 * 1000))
    .toString()
    .padStart(2, "0");
  const minutes = Math.floor((timeGap / (60 * 1000)) % 60)
    .toString()
    .padStart(2, "0");
  const seconds = Math.floor((timeGap / 1000) % 60)
    .toString()
    .padStart(2, "0");
  return `${hours}시간 ${minutes}${seconds}초`;
}

유저 마지막 뽑은 시간 저장하기

  • src\api\userAPI.ts
    • 버튼 핸들러에서 실행합니다.
/** firestore에서 lastDrawTime 정보를 업데이트합니다. */
const updateUserDrawTime = async (user: userObjDTO) => {
  if (!user) {
    throw new Error("사용자 정보가 없습니다.");
  }

  try {
    const { uid } = user;
    const { collection } = await getFirestoreRefObject();

    const userRef = collection.where("userId", "==", uid);

    const snapshot = await userRef.get();

    if (snapshot.empty) {
      throw new Error("해당 유저 정보가 없습니다.");
    }

    const userDoc = snapshot.docs[0];

    const updatedData = {
      lastDrawTime: new Date(),
    };

    await userDoc.ref.update(updatedData);
  } catch (err) {
    console.log(err);

    throw new Error("사용자 DB 업데이트에 실패했습니다.");
  }
};

setInterval로 시간을 확인합니다.

- `src\components\timer\hooks\useTimer.ts`
useEffect(() => {
  const interval = setInterval(async () => {
    const gap = await getTimeGap();
    setTimeGap(formatTimeGap(gap));
    setIsOverHour(gap >= oneHour);
  }, 1000);

  return () => clearInterval(interval);
}, [oneHour]);

✨ [UI] 버튼 비활성화 ✨

1시간 이내라면 버튼을 비활성화 하는 CSS

<button
  onClick={
    isOverHour
      ? buttonClickHandler
      : () => alert("아직 뽑을 수 없어요:(")
  }
  className="random-pokemon relative -mt-2 hover:scale-125 transition-all active:scale-50"
>
  <Image
    className={isOverHour ? "" : "opacity-25"}
    width={40}
    src={pokeball}
    alt="몬스터볼"
  />
</button>

image

Firestore : 유저별 리스트 _ 230213

Firestore : 유저별 리스트 _ 230213

  • 23년 2월 13일, 저녁 8시~12시
  • 로그인한 유저별로 포켓몬 리스트를 관리하도록 DB를 변경했습니다.
  • #12

image


🗂 작업내용

1. npm 설치 : "@firebase/firestore", "express-session"

  • firebase 관련 타입 정리
  • 로그인 uid 관리

2. DB 구조 변경

  • 유저별 포켓몬 리스트 관리 가능하게 변경
  • 유저별 포켓몬 리스트 관련, api call 내용 작성
  • timstamp 저장

3. UI

  • 몬스터볼 버튼 추가
  • 포켓몬 리스트 반응형 : tailwind.config에 미디어쿼리 추가,

image


✨ npm 설치 : 파이어베이스 타입, 세션 스토리지 ✨

NextJS(SSR)와 저장소

  • 서버사이드 랜더링(SSR)에서 사용자 정보를 저장하려면 window.localStorage가 아닌 다른 저장소를 사용해야 합니다.
    • 예를 들어, 간단한 서버 상태 관리를 위해서는 서버 상에서 관리할 수 있는 저장소를 사용할 수 있습니다. 예를 들어, 세션 또는 쿠키를 사용할 수 있습니다. 서버사이드 랜더링 상에서 사용자 정보를 저장하는 것은 개발자의 선택에 따라 다양한 방법이 있을 수 있습니다.

      저장소 SSR / NextJS에서 사용할 때 npm install 패키지
      LocalStorage 사용할 수 없습니다. x
      SessionStorage 패키지를 설치합니다. express-session
      Cookie 패키지를 설치합니다. cookie-parser

그래서 어디에 저장하나요?

  1. 애플리케이션에 접근 했을 때, _app.ts에서 최초로 로그인을 확인함.
  2. 쿠키: 만료 기한을 설정하거나, uid를 따로 서버에 request로 넘길 필요가 없기 때문에, 쿠키에 저장하지 않습니다.
  3. 세션: 로그인이 되었을 때, 유저 정보를 확인하기 때문에 세션이 끝났을 때, 유지할 필요가 없습니다. 따라서 세션 스토리지가 적합합니다.

uid를 SessionStorage에 저장합니다.

useEffect(() => {
  authService.onAuthStateChanged((user) => {
    if (user) {
      setIsLoggedIn(true);
      saveUserData(user); // 서버에 user 정보가 없다면 저장
      sessionStorage.setItem("user", user.uid); // 세션에 uid 저장
    } else {
      setIsLoggedIn(false);
      sessionStorage.removeItem("user");
    }
    setInit(true);
  });
}, []);

파이어스토어 타입 설치 🔥

npm install firebase @firebase/firestore @types/firebase
import firebase from "firebase/compat/app";

export interface userProps {
  isLoggedIn: boolean;
  userObj: userObjDTO;
}

export type userObjDTO = firebase.User | null;

export interface GetUserRef {
  uid: string | null;
  collection: firebase.firestore.CollectionReference<firebase.firestore.DocumentData>;
  userRef: firebase.firestore.Query<firebase.firestore.DocumentData>;
  user: firebase.firestore.QueryDocumentSnapshot<firebase.firestore.DocumentData>;
}

✨ DB 구조 변경 ✨

  • NoSQL 만세
{
  userId: string,
  userName: string,
  providerId: string,
  totalPokemonNumber: number,
  lastLoggedIn: firebase.firestore.Timestamp;,
  lastDrawTime: firebase.firestore.Timestamp;,
  pokemonList: PokemonDTO[],
}

데이터를 수정하기

  • https://cloud.google.com/firestore/docs/manage-data/add-data?hl=ko
  • Firestore 는 단일 문서의 데이터를 수정하는 것이 아니라, 문서의 새 버전을 생성하여 저장하는 방식으로 동작합니다.
    • 그래서 addPokemonToList 함수에서 pokemonList 를 수정하기 위해서는 우선 userRef.get()을 통해 user 문서의 새 버전을 생성한 다음, pokemonList 배열에 새로운 PokemonDTO 객체를 추가한 다음, 다시 문서를 저장해야 합니다.
/** 새 포켓몬을 추가합니다. */
const addPokemonToList = async (pokemon: PokemonDTO) => {
  const { userRef } = await getFirestoreRefObject();

  try {
    const user = (await userRef.get()).docs[0];

    // 새로운 pokemonList 배열을 생성
    const updatedPokemonList = [...user.data().pokemonList, pokemon];

    // 새로운 버전의 user 문서 저장
    await user.ref.update({
      pokemonList: updatedPokemonList,
    });
  } catch (error) {
    console.error("새 포켓몬 저장에 실패 했습니다 : ", error);
  }
};

포켓몬 리스트 함수 변경

  • 이전 코드에서, fireStore관련 코드, 정렬 등 서버와 관련된 코드 제거했습니다.
    • 받은 배열을 출력하는 코드과 타입 체크만 남깁니다.
    • 정렬과 관련된 코드를 데이터를 출력하는 함수에서 사용하지 않습니다. (no 순으로 페이지네이션 할 수 없음)
/** pokemonList을 return합니다. */
const fetchPokemonList = async (): Promise<FBPokemonDTO[]> => {
  return await fetchPokemonDB();
};

타임스탬프 저장하기

  • 타임스탬프 시간 저장
catched_at: firebase.firestore.Timestamp.fromDate(new Date()),
  • 타임스탬프 타입 체크
import firebase from "firebase/compat/app";
...
const pokemonData: PokemonDTO = {
  no,
  names: mapNamesObj,
  imgUrl,
  catched_at: firebase.firestore.Timestamp.fromDate(catched_at),
};

✨ UI ✨

몬스터볼 버튼 추가

  • 마우스오버 시 scale 125%
  • 클릭 시 scale 50%
<button
  onClick={updateIdNo}
  className="random-pokemon relative -mt-2 hover:scale-125 transition-all active:scale-50"
>
  <Image width={40} src={pokeball} alt="몬스터볼" />
</button>

image

반응형 추가

  • 포켓몬 리스트 가로 아이템 갯수 변화
    • 3 => 5 => 3 => 2
theme: {
  extend: {
    ...
    screens: {
      xs: "500px", // 추가함
      xxs: "360px", // 추가함
    },
  },
<ul className="grid grid-cols-2 xxs:grid-cols-3 xs:grid-cols-5 md:grid-cols-3">
  {pokemonList.map((item) => (
    <li key={item.no} className="text-xxs">
      <Pokemon pokemon={item} />
    </li>
  ))}
</ul>

타이머 개선, Vercel 배포 _ 230222

타이머 개선, Vercel 배포

  • 23년 2월 22일, 저녁 10시 40분~12시
  • [리팩토링] 타이머, 제한 시간을 limit 값에 따라 동적으로 바뀌도록 개선
  • [UI] 애니메이션 조금 추가
  • Vercel로 NextJS 프로젝트 배포
  • #22

image


🗂 작업내용

[리팩토링] 타이머, 제한 시간을 limit 값에 따라 동적으로 바뀌도록 개선

[UI] 애니메이션 조금 추가

Vercel로 NextJS 프로젝트 배포


✨ [리팩토링] 타이머, 제한 시간을 limit 값에 따라 동적으로 바뀌도록 개선 ✨

  • limit 값에 맞춰 동적으로 변화하도록 개선

    • src\components\timer\hooks\useTimer.ts

      const oneHour = 60 * 60 * 1000;
      const fiveMinite = 1 * 60 * 1000;
      const limit = fiveMinite; // 이 값을 로직에 사용
      ...
      const gap = await getTimeGap(limit); // gap을 구할 때, limit를 인수로 사용
      setTimeGap(formatTimeGap(gap));
      setLastTime(formatTimestamp(userData.lastDrawTime));
      setIsOverLimit(gap <= 0);
  • getTimeGap에서 limit를 사용합니다.

    • src\api\userAPI.ts

      const getTimeGap = async (limit: number) => {
        const { userRef } = await getFirestoreRefObject();
        const userData = (await userRef.get()).docs[0].data();
      
        const timestamp = userData.lastDrawTime;
        const lastDrawTime = new Date(
          timestamp.seconds * 1000 + timestamp.nanoseconds / 1000000
        );
      
        const now = new Date();
        const elapsed = now.getTime() - lastDrawTime.getTime();
        const remaining = limit - elapsed;
      
        return remaining;
      };
      

✨ [UI] 애니메이션 조금 추가 ✨

  • 애니메이션 추가
    • 가능할 때 ping + pulse

    • 불가능할 때 pulse

      <div
        className={
          "w-3 h-3 rounded-lg inline-block mx-2" +
          (isOverLimit
            ? " bg-green-400 animate-pulse animate-ping" // 애니메이션 추가
            : " bg-rose-600 animate-pulse")
        }
      ></div>
      {lastTime}
      ...
      <p className="text-indigo-600 h-6">
        {isOverLimit ? "" : `${limit / 60 / 1000}분 제한까지 ${timeGap}`} // 고정된 값이 아니라, 변수 limit를 출력
      </p>

image


✨ Vercel로 NextJS 프로젝트 배포 ✨

  • aws, docker 등 여러 곳에 사용도가 높은 깃헙 액션을 사용하고 싶었으나,
    들이는 시간, 시간 대비 결과물을 고려하여 vercel을 사용해 배포하였습니다.
    • 배포에 5분 걸렸습니다. Next 👉 Vercel !

외부 도메인 이미지 img 태그 사용, 일부 CSS 변경, 추가 버그 수정 필요 _ 230315

외부 도메인 이미지 img 태그 사용, 일부 CSS 변경, 추가 버그 수정 필요 _ 230315

  • 23년 3월 15일, 저녁 10시~1시
  • 외부 도메인 관련 Next Image 버그픽스
  • 일부 CSS, Timer 관련 버그 수정
  • #32
  • #33

🗂 작업내용

외부 도메인 관련 Next Image 버그픽스 : 공식문서 학습 및 Next/Image 사용 보류

일부 CSS 버그 수정

일부 Timer 버그 수정


외부 도메인 사용 관련, Next Image 버그 수정

  • 이미지 400에러 관련, 배포 환경과 개발 환경의 Src는 동일한 점 확인
    image

Next 이미지가 배포 환경에서 400에러가 뜨는 이유

  • 🚫 Vercel로 배포한 Next.js 프로젝트에서 이미지가 400 에러가 발생하는 경우, 일반적으로 다음과 같은 이유들이 있습니다.
  1. 이미지 경로 문제: 이미지 경로가 올바르지 않거나, 파일 이름이 잘못되었을 수 있습니다. 이미지 경로를 확인하고 올바른지 확인해 보세요.
  2. 권한 문제: 이미지에 대한 권한이 없거나, 이미지가 private 저장소에 저장되어 있어 접근이 불가능할 수 있습니다. 이미지 파일에 대한 권한을 확인하고 필요하다면 수정하세요.
  3. 최적화 및 로딩 전략: Next.js는 기본적으로 이미지 최적화 기능을 제공합니다. 이 기능을 사용하려면 next/image 컴포넌트를 사용해야 하며, 외부 이미지의 경우 next.config.js 파일에 외부 도메인을 추가해야 합니다. 설정을 확인하고 필요한 경우 수정하세요.
  4. 캐싱 문제: Vercel은 캐싱 전략을 사용하여 웹 사이트의 성능을 최적화합니다. 이미지 파일의 캐싱이 올바르게 처리되지 않은 경우 문제가 발생할 수 있습니다. 캐싱 설정을 확인하고 필요하다면 수정하세요.
  5. 서버 문제: 이미지가 저장된 서버에 문제가 발생한 경우, 400 에러가 발생할 수 있습니다. 서버 로그를 확인하고 문제를 해결해야 합니다.

관련 버그 수정을 위해 공식문서를 참고하여 next.config.js를 수정

  1. domain
    iamges: {
    	domain: ["raw.githubusercontent.com"]
    }
  2. remotePatterns
    images: {
      remotePatterns: [
        {
          protocol: "https",
          hostname: "raw.githubusercontent.com",
          pathname: "/PokeAPI",
        },
      ],
    },
    

Next Image의 Loader와 Src

이미지 컴포넌트의 자동 이미지 최적화 사용하지 않기

  • 이미지 최적화 일단 사용하지 않고, 추후에 다시 알아보기
    방법A : Image에 직접 unoptimized 추가
{/* 포켓몬 이미지 */}
<Image
  loader={() => imgUrl}
  src={imgUrl}
  alt={"뭘까요?"}
  width={40}
  height={40}
  className={`img w-full object-contain transition-opacity ${
    imageLoaded ? "opacity-100" : "opacity-0"
  }`}
  unoptimized={true}
  onLoadingComplete={imageLoadHandler}
/>

방법B : config에 다음과 같이 추가

module.exports = {
  images: {
    loader: 'imgix',
    path: 'https://example.com/myaccount/',
  },
}

문제점: 로컬에서는 최적화 되지 않지만, 배포 단계에서는 srcset 적용
image

해결되지 않으므로 img 태그로 수정하였습니다.

📌 최적화 관련
unoptimized 속성을 사용하면, Next.js는 최적화된 이미지를 생성하지 않고 src 속성에 있는 원본 이미지를 사용합니다. 그러나 이 속성이 있어도 Image 컴포넌트는 여전히 **srcset**을 생성합니다. 이는 브라우저가 사용할 수 있는 다양한 이미지 크기와 해상도를 제공하기 위해서입니다.
만약 **srcset**을 사용하지 않으려면, Image 컴포넌트가 아닌, 기본 HTML의 <img> 태그를 사용해야 합니다. 다음과 같이 작성할 수 있습니다.

<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/211.png" alt="뭘까요?" width="50" height="50" loading="lazy" className="img w-full object-contain transition-opacity opacity-0" />

이렇게 하면 이미지 최적화를 사용하지 않고 원본 이미지를 로드하고 **srcset**이 생성되지 않습니다. 이 방법을 사용할 때는 주의해야 할 점이 있습니다. Next.js의 이미지 최적화를 사용하지 않으면, 이미지 로딩 성능이 저하될 수 있으며, 다양한 기기에 대한 이미지 크기 최적화가 제공되지 않습니다.

  • 이미지 최적화보다 이미지가 정상적으로 나오는 것이 중요하므로 태그로 변경하였습니다.
    • 이후 추가 학습 필요

일부 CSS 버그 수정

  • 배경색이 나오지 않던 부분 수정
    • tailwind.config.js에서 설정한 색상이 인식되지 않을 때
      • 설정한 theme이 color인지 backgroundColor인지 key 확인!

타이머 버그 수정

  • 타이머가 갱신될 때 1초 동안 "-1시간 -1분 -1초" 나오던 버그
    image

Tailwind CSS 작업 진행

Tailwind CSS 작업 진행 (미완)

  • 23년 2월 22일, 저녁 10시~12시
  • #25
  • [DALL E 2] UI/UX 디자인
  • [Tailwind] CSS 작업

image
image


🗂 작업내용

[DALL E 2] UI/UX 디자인

[Tailwind] CSS 작업


✨ [DALL E 2] UI/UX 디자인 ✨

DALL E 2에서 만든 이미지를 바탕으로 구상

  • 헤더 있음 ⇒ 유저 이름 왼쪽

image

  • 색상은 이렇게

image

  • 새로 뽑을 포켓몬 표시 ⇒ 누르면 뽑히는 애니메이션
  • 클릭 가능한 포켓몬은 빨간 테두리

image

  • 좌우 버튼 빨간색 작게

image

  • 상세 페이지는 이렇게 : 버튼은 빨강

image


✨ [Tailwind] CSS 작업 ✨

그라디언트 + 색상 지정

  • 다음과 같이 사용합니다.
    • bg-gradient-to-방향
    • to-[HEX 컬러] from-[HEX 컬러]
<div className="bg-gradient-to-b from-[#49a5b5] to-[#244952]">

tailwind 스크롤바 없애기 = 플러그인

  • 🗒️ tailwind-scrollbar-hide
    플러그인은 Tailwind CSS에서 제공하는 공식 플러그인으로, 스크롤바를 숨기는 기능을 제공합니다. 이 플러그인을 사용하면 다음과 같은 클래스를 적용할 수 있습니다.
<div class="scrollbar-hide"></div>

이를 통해 해당 요소의 스크롤바를 숨길 수 있습니다. tailwind-scrollbar-hide
플러그인을 사용하려면 먼저 tailwind.config.js파일에서 plugins 항목에 require('tailwind-scrollbar-hide')를 추가해야 합니다.

// tailwind.config.js

module.exports = {
  // ...
  plugins: [
    // ...
    require('tailwind-scrollbar-hide')
  ],
};

inner 그림자 커스텀

box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);

Tailwind CSS 작업 & 타이머 개선 _ 220223

Tailwind CSS 작업 & 타이머 개선

  • 23년 2월 23일, 저녁 10시~12시20분
  • [tailwind] CSS 작업 (feat. NextJS)
  • 타이머 리랜더링 개선
  • #26


🗂 작업내용

[tailwind] CSS 작업 (feat. NextJS)

타이머 리랜더링 개선


✨ [tailwind] CSS 작업 (feat. NextJS) ✨

tailwind와 next 사용 시 사이즈 문제점

  • "#__next" div 엘레먼트는 랜더링 프로세스 중 생성되는 요소이므로 직접 className을 사용하여 수정할 수 없습니다.
<div id="__next"></div>

🗒️ <div id="__next">는 Next.js의 렌더링 프로세스 중에 자동으로 생성되는 요소입니다. 이 요소의 높이값을 직접 지정할 수 없습니다. 대신, 이 요소의 부모 요소에 높이값을 지정하거나, 높이를 동적으로 조절하는 JavaScript 코드를 사용할 수 있습니다.

일반적으로, 페이지 컨텐츠의 높이에 맞게 자동으로 높이가 조절되기 때문에, <div id="__next">의 높이값을 따로 조절할 필요는 없을 것입니다. 만약 페이지 컨텐츠가 화면에 꽉 차지 않아서 <div id="__next">의 높이가 짧게 나타난다면, 이는 페이지 컨텐츠의 스타일을 수정하여 해결할 수 있습니다. 예를 들어, 페이지 컨텐츠의 min-height를 100vh로 설정하면 화면 전체를 채우게 됩니다.

직접 높이값 100%로 설정할 수 없으므로

A. 부모 요소에 CSS 설정
B. 현재 디자인 같은 경우, 1) 하단 영역의 빨간색을 없애고 2) 페이지네이션을 absolute로 bottom에 붙입니다.

<div className="h-screen sm:px-4 md:px-0 sm:py-5 md:py-0 flex flex-col justify-between items-center bg-gradient-to-b from-[#49a5b5] to-[#244952]">
      <div className="w-full md:max-w-md flex-1 sm:rounded-lg flex flex-col drop-shadow-xl scroll-p-2 overflow-y-scroll  scrollbar-hide overflow-x-hidden scroll bg-red">
        <header className="h-10 flex items-center justify-between bg-[#475e63] text-[#49a5b5]">
        ...

하단 이미지 부분 CSS 작업

  • 기존 : 배경이 빨간색, 리스트 영역이 흰색 (하단 공간이 남아서 빨간색으로 보입니다.)
  • 변경하기 : 상단 부분을 gradient로 반 나눠서 처리하기(실패)

tailwind 사용 미숙으로 인해, HTML element의 영역을 잡는 데 어려움이 있습니다.

  • src\styles\index.css 에서 직접 css style을 지정하는 방식으로 해결했습니다.
.main-section {
  height: calc(100vh - 10%);
}

오늘자 CSS 수정 결과


✨ 타이머 리랜더링 개선 ✨

문제점

  • isOverLimit가 true인 상태(뽑기 가능한 상태)일 때는 interval을 clear해야 합니다.
    • 불필요한 리랜더링

useEffect에서 isOverLimit가 true인 경우를 추가합니다.

```tsx
// 타이머 인터벌
useEffect(() => {
  const interval = setInterval(async () => {
    const gap = await getTimeGap(limit);
    setTimeGap(formatTimeGap(gap));
    setIsOverLimit(gap <= 0);
  }, 1000);

  if (isOverLimit) {
    clearInterval(interval);
  }

  return () => clearInterval(interval);
}, [limit, isOverLimit]);
```

timer와 뽑기 버튼에 각각 isOverLimit과 setIsOverLimit을 적용합니다.

  • useTimer에 returnType을 추가합니다.
export interface TimerReturnType {
  lastTime: string;
  timeGap: string;
  isOverLimit: boolean;
  setIsOverLimit: Dispatch<SetStateAction<boolean>>;
  limit: number;
}
  • Timer.tsx
    • 커스텀 훅 : 부모 컴포넌트에서 전달하도록 수정하였습니다.
export default function Timer({ timer }: { timer: TimerReturnType }) {
  const { lastTime, timeGap, isOverLimit, limit } = timer;
  ...
  • PokemonNew.tsx
    • 버튼 클릭 핸들러에 setState 추가
const buttonClickHandler = () => {
  updateIdNo();
  updateUserDrawTime();
  setIsOverLimit(false); 
};
  • 컴포넌트간 공통된 값의 커스텀 훅을 사용하기 위해서 부모 컴포넌트에서 useTimer를 Import타이머가 작동할 때 부모 컴포넌트의 모든 컴포넌트가 리랜더링 되는 이슈가 생김 ⇒ react.memo 사용했지만 해결되지 않음 ⇒ 더 알아보기
export default React.memo(PokemonList);

로딩 시 스켈레톤 UI 추가 및 CSS 일부 수정 _ 230313

로딩 시 스켈레톤 UI 추가 및 tailwind CSS 일부 수정 _ 230313

  • 23년 3월 13일, 저녁 8시~1시반
  • [tailwind] CSS 작업
  • [react-loading-skeleton] 스켈레톤 UI
  • [react-icons] 로그아웃 아이콘 추가
  • #30
  • [slow 3G GIF]
    230313_1

🗂 작업내용

로그아웃 버튼: 아이콘으로 변경하고 헤더로 이동

Pokemon.ts에 CSS 관련하여 전달할 props 추가

로그인 페이지, 상세 페이지 레이아웃 깨지는 부분 수정

[UI/UX] 화면 로딩 시에 스켈레톤 UI 적용


✨ [tailwind] CSS 수정 내용 ✨

로그아웃 버튼: 아이콘으로 변경하고 헤더로 이동

image

Pokemon.ts에 CSS 관련하여 전달할 props 추가

  • 배경색 추가: #ffffff인 경우 추가
  • 텍스트 크기: xxs, sm
  • 텍스트 한 줄로 변경
  • 글자 태그를 숨기지 않는 경우 추가 등
interface Pokemon {
  pokemon: PokemonDTO | FBPokemonDTO;
  background?: string;
  hideText?: boolean;
  textSize?: string;
}

export default function Pokemon({
  pokemon,
  background = "#ebf3f3",
  hideText = true,
  textSize = "text-xxs",
}: Pokemon) {
  const { names, imgUrl, no } = pokemon;
  const isCachedRange = no > 0 && no <= 151;

  // 스타일 props 적용
  const backgroundColor = `bg-[${background}]`;
  const textOpacity = hideText ? "opacity-0" : "";
  const textColor = isCachedRange ? "text-green-300" : "text-zinc-50";
  const border = isCachedRange ? "border-2 border-red" : "";
  ...

로그인 페이지, 상세 페이지 레이아웃 깨지는 부분 수정

image


✨ [react-loading-skeleton] 화면 로딩 시에 스켈레톤 UI 적용 ✨

화면 로딩 시에 스켈레톤 UI 적용

  • 패키지 설치 : react-loading-skeleton
    • 부모의 height를 사용하는 것 같음 : padding 100%를 인식하지 못함
      • width에 맞는 height === width 비율 구하기
      • padding 100%로 부모 영역을 1:1로 잡아놓고, absolute 박스의 100% 100%를 크기로 사용합니다.
      const singleItem = (
      <div>
        {/* 부모의 height를 사용하는 것 같음 : padding으로 영역 잡기 불가능 => width에 맞는 height === width 비율 구하기... */}
        <div className="w-full h-0 pb-[100%] mr-0.5 flex-shrink-0 relative shadow-sm">
      
          {/* 높이를 잡는 박스 */}
          <div className="w-full h-full absolute-center">{children}</div>
          {/* 원을 그리기 위해 사용하는 absolute 박스 */}
          <div className="w-full h-full pt-3 p-2 absolute-center z-10 ">
            <div className="w-full h-full rounded-full bg-light-blue opacity-60" />
          </div>
        </div>
      </div>

slow 3G 로딩 예시

230313_2
230313_1

Bugfix & Refactor: 불필요한 firebase 요청 수정, useTimer & userAPI 관련 리팩토링 _ 230317

Bugfix & Refactor: 불필요한 firebase 요청 수정, useTimer & userAPI 관련 리팩토링 _ 230317

  • 23년 3월 17일, 저녁 9시~1시
  • [bugfix] Timer가 작동할 때 매 리랜더링 시 발생하는 firebase 요청 수정
  • [refactor] 함수 내용 정리 및 약간의 리팩토링
  • #37
  • #35

🗂 작업내용

1. Timer가 작동할 때 매 리랜더링 시 발생하는 firebase 요청 수정

2. 버그 원인과 발생 위치 찾기(요청 확인하기)

3. userAPI & pokemonAPI 약간의 리팩토링

4. 요청 관련 추가적인 개선이 필요합니다.

5. Error: Objects are not valid as a React child

6. useTimer 관련 주석 추가 및 약간의 리팩토링


Bugfix ✨

1. 타이머가 작동할 때 1초마다 반복적으로 요청이 일어납니다.

  • 요청이 일어나는 곳을 확인하기 위해 src/api 폴더내의 요청을 확인하였습니다.
    • 또한 nextJS에서 api폴더는 server side 코드를 구현하기 위해서 정해진 default folder명이기 때문에 api폴더를 services로 변경했습니다.

2. 버그 원인과 발생 위치 찾기(요청 확인하기)

함수 이름 설명 파이어베이스와 통신 파일 위치 내용
getFirestoreRefObject Firestore 참조 객체를 가져옵니다. Yes userAPI  
savePokemonToDB 포켓몬 데이터를 DB에 저장합니다. Yes pokemonAPI  
addPokemonToList 포켓몬을 목록에 추가합니다. No pokemonAPI savePokemonToDB에서 호출
setPaginationFromUserRef 유저 참조를 기반으로 페이지네이션을 설정합니다. Yes pokemonAPI  
fetchPokemonDB 포켓몬 데이터를 DB에서 가져옵니다. Yes pokemonAPI  
initUserObject 유저 객체를 초기화합니다. Yes userAPI  
saveUserData 유저 데이터를 저장합니다. Yes userAPI  
updateUserDrawTime 유저의 마지막 포켓몬 뽑기 시간을 업데이트합니다. Yes userAPI  
getTimeGap 유저가 마지막으로 포켓몬을 뽑은 시간과 현재 시간의 차이를 구합니다. No여야 함 userAPI 버그가 일어나는 문제 함수
  • src/api/userAPIgetTimeGap
    • 이전 코드
// 마지막 뽑은 시간과 시간 차이를 구합니다.
const getTimeGap = async (limit: number) => {
  const { userRef } = await getFirestoreRefObject();
  if ((await userRef.get()).docs[0] === undefined)
    return new Error("유저 정보를 가져오지 못했습니다.");

  const userData = (await userRef.get()).docs[0].data();

  const timestamp = userData.lastDrawTime;
  const lastDrawTime = new Date(
    timestamp.seconds * 1000 + timestamp.nanoseconds / 1000000
  );

  const now = new Date();
  const elapsed = now.getTime() - lastDrawTime.getTime();
  const remaining = limit - elapsed;

  return remaining;
};
  • 확인 결과, getTimeGap 함수에서 **"사용자가 마지막으로 포켓몬을 뽑은 시간"**을 확인하기 위한 불필요한 요청을 보내고 있습니다.
  • 해당 함수는 interval 안에서 동작합니다.
    • 유저가 마지막으로 포켓몬을 뽑은 데이터를 반복적으로 가져올 필요가 없습니다.
    • 파이어베이스 요청을 제거하고, 또한 기능에 맞도록 useTimer 커스텀 훅으로 위치를 이동합니다.

3. 변경 내용 : userAPI & pokemonAPI 약간의 리팩토링

  • getTimeGap
// 시간 차이 number를 구합니다.
const getTimeGap = async (limit: number, lastTimestamp: Timestamp) => {
  const lastDrawTime = new Date(
    lastTimestamp.seconds * 1000 + lastTimestamp.nanoseconds / 1000000
  );

  const now = new Date();
  const elapsed = now.getTime() - lastDrawTime.getTime();
  const remaining = limit - elapsed;

  return remaining;
};
  • addPokemonToList
    • 또한 함수들을 확인한 결과, addPokemonToList는 savePokemonToDB 하위에서 화면에 나타나는 list가 변경되는 함수입니다.
    • 요청을 새로 보내지 않고, 부모 함수에서 userRef를 전달받아 사용합니다.

4. 요청 관련 추가적인 개선이 필요합니다.

  • userAPI에서 user를 확인하기 위해 불필요하게 반복적인 요청을 보내는 부분이 있습니다.
  • auth 관련 코드 또한 app에서 분리하여 커스텀 훅을 작성하는 것이 좋겠습니다.
  • 현재 전역 스토어를 사용하고 있지 않기 때문에, 커스텀 훅을 사용해 user와 auth data를 한 번에 보관하도록 작성할 계획입니다.

5. 추가 버그 수정

Error: Objects are not valid as a React child

  • Error: Objects are not valid as a React child (found: object with keys {seconds, nanoseconds}). If you meant to render a collection of children, use an array instead.
    image
  • 메시지에서 확인할 수 있듯이, Timestamp를 그대로 interface 객체에 담아서 return하려 할 때 생기는 에러입니다.
export interface TimerReturnType {
  // Timestamp를 그대로 return할 수 없음 : nt {seconds: 1679055411, nanoseconds: 500000000}
  // lastDrawTimestamp: Timestamp | undefined;
  timeGapText: string;
  isOverLimit: boolean;
  setIsOverLimit: Dispatch<SetStateAction<boolean>>;
  limit: number;
}
  • 다음과 같이 사용할 수 있습니다. 다만 현재 해당 타입을 return하지 않으므로 삭제하였습니다.
export interface TimerReturnType {
  timeGapText: string;
  isOverLimit: boolean;
  setIsOverLimit: Dispatch<SetStateAction<boolean>>;
  limit: number;
  lastDrawTimestamp: firebase.firestore.Timestamp | undefined;
}

Refactoring ✨

6. useTimer 관련 주석 추가 및 약간의 리팩토링

  • 상수를 함수 스코프 바깥으로 이동했습니다.
  • formatter 등 변수명 일부 수정
  • getTimeGap 기능을 버그 수정 후 useTimer로 이동했습니다.
  • timestamp는 현재 커스텀훅에서만 사용하고 return하지 않도록 했습니다.
  • 화면에 나타날 텍스트 두 경우를 모아놓았습니다.
  • 각 useEffect 기능에 대한 주석을 추가했습니다.
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { Timestamp } from "firebase/firestore";

import { getFirestoreRefObject } from "@/services/userAPI";
import {
  formatTimeGapToLocalString,
  formatTimestampWithColon,
} from "@/utils/timeFormatter";

export interface TimerReturnType {
  timeGapText: string;
  lastDrawText: string;
  isOverLimit: boolean;
  setIsOverLimit: Dispatch<SetStateAction<boolean>>;
  LIMIT: number;
}

const ONE_HOUR = 60 * 60 * 1000;
const ONE_MINITE = 1 * 60 * 1000;
const LIMIT = ONE_MINITE;

export default function useTimer(): TimerReturnType {
  const [loading, setIsLoading] = useState(false);
  const [lastDrawTimestamp, setLastDrawTimestamp] = useState<
    Timestamp | undefined
  >();
  const [isOverLimit, setIsOverLimit] = useState(false);

  // 화면에 출력할 텍스트
  const [timeGapText, setTimeGapText] = useState("");
  const [lastDrawText, setLastDrawText] = useState("😃");

  /**
   * 현재 Date와, 마지막 타임스탬프 간의 시간 차이를 구합니다.
   * */
  const getTimeGap = async (limit: number, lastTimestamp: Timestamp) => {
    const lastDrawDate = new Date(
      lastTimestamp.seconds * 1000 + lastTimestamp.nanoseconds / 1000000
    );

    const now = new Date();
    const elapsed = now.getTime() - lastDrawDate.getTime();
    const remaining = limit - elapsed;

    return remaining;
  };

  /** <타이머 초기화>
   * 0. 컴포넌트가 amount하면 타이머를 초기화합니다.
   * 1. firebase에서 lastDrawTime를 패칭합니다.
   * 2. 현재 시간과의 gap을 구합니다.
   * 3. 화면에 출력할 timeGapText, lastDrawText
   *    그리고 타이머 기능에 사용할 lastDrawTimestamp, isOverLimit를
   *    초기화합니다.
   * 4. 별개의 useEffect인 인터벌 기능이 작동해
   *    isOverLimit가 변화하면 다시 초기화합니다.
   * */
  useEffect(() => {
    setIsLoading(true);

    (async function () {
      const { userRef } = await getFirestoreRefObject();
      const userData = (await userRef.get()).docs[0].data();
      const timestamp: Timestamp = userData.lastDrawTime;

      const gap = await getTimeGap(LIMIT, timestamp);
      if (typeof gap !== "number" || gap >= ONE_MINITE || gap === 0) return;

      setTimeGapText(formatTimeGapToLocalString(gap));
      setLastDrawText(formatTimestampWithColon(timestamp));

      setLastDrawTimestamp(timestamp);
      setIsOverLimit(gap < 0);

      setIsLoading(false);
    })();
  }, [isOverLimit]);

  /** <타이머 인터벌>
   * lastDrawTimestamp이 존재하고, loading 중이 아닐 때 인터벌합니다.
   * 1초마다 gap을 확인하고, 관련 state를 업데이트 합니다.
   * isOverLimit이 변경되었을 때, true라면 interval을 clear합니다.
   */
  useEffect(() => {
    const interval = setInterval(async () => {
      if (!lastDrawTimestamp || loading) return;

      const gap = await getTimeGap(LIMIT, lastDrawTimestamp);
      if (typeof gap !== "number") throw new Error("타이머 에러");

      const newIsOver = gap < 0;
      if (newIsOver) {
        setTimeGapText("");
        !isOverLimit && setIsOverLimit(true);
      } else {
        setTimeGapText(formatTimeGapToLocalString(gap));
        isOverLimit && setIsOverLimit(false);
      }
    }, 1000);

    // interval clear
    if (isOverLimit) {
      clearInterval(interval);
    }

    return () => clearInterval(interval);
  }, [isOverLimit, lastDrawTimestamp, loading]);

  return {
    timeGapText,
    lastDrawText,
    isOverLimit,
    setIsOverLimit,
    LIMIT,
  };
}

포켓몬 리스트 추가, 관련 hook & query key 작업 _ 230212

포켓몬 리스트 추가, 관련 hook & query key 작업

  • 23년 2월 12일, 저녁 3시~9시
  • 유저가 포켓몬을 뽑았을 때, firestore DB에 데이터를 저장하고, 하단 리스트에 새 포켓몬을 추가합니다.
  • #10

🗂 작업내용

1. [firestore] collection pokemonDB에 데이터를 패칭 및 저장

  • 유저별로 관리할 수 있는 형태로 변경해야 합니다.
  • 데이터 저장하는 시점을 수정하였습니다.

2. [react-query] 리스트 관련 query key 추가 및 캐시 관리

  • 커스텀훅에 대한 이해도가 낮아 발생한 트러블 슈팅 (feat. queryClient.clear())
    • 주요 내용 : 리스트가 리랜더링 되지 않음
  • 이전 데이터를 보관해, 로딩 시 캐싱된 리스트를 보여줍니다.

3. [view] 화면에 리스트 출력

  • 포켓몬 no 순서대로 나오도록 정렬합니다.
  • 스크롤 추가


✨ Firestore ✨

대상 메소드 결과 액션
db.collection(콜렉션명) add promise로 documentReference를 리턴 document 저장
db.collection(콜렉션명) get promise로 querySnapShot을 리턴 collection 가져오기
querySnapShot forEach querySnapShot 각 document를 순회

collection pokemonDB에 데이터를 패칭 및 저장

  • 다음 위치에서 저장

    • src\components\pokemon\hooks\usePoketmonQuery.ts
      function updateIdNo(): void {
        setIdNo({ curr: idNo.next, next: getId() }); 
      	    // 쿼리키에 사용하는 state 업데이트 => 쿼리키가 변경되므로 새 데이터 패칭됨
          // setState는 비동기로 동작하므로, 현재 함수가 끝난 후 값이 변경될 것
      
        queryClient.clear(); // unactive된 garbage 내용 지우기
        savePokemonDB(newPokemon); // 현재 쿼리키의 값을 전달
      }
  • api call 위치

    • src\api\poketmonAPI.ts
    export const savePokemonDB = async (payload: PokemonDTO) => {
      try {
        await dbService.collection("pokemonDB").add(payload);
      } catch (err) {
        throw new Error("포켓몬 저장 실패");
      }
    };
    • promise로 documentReference를 리턴합니다.

      image

  • 상세 에러는 에러 핸들러에서 처리

    • src\react-query\queryClient.ts

db에서 가져오기 : get(), querySnapShot

  • useQuery와 쿼리 키입니다.🤔

    const { data: pokemonList = [] } = useQuery(
        [queryKeys.pokemonList, idNo.curr],
        () => {
          return fetchPokemonList();
        }
      );
  • 패치하는 함수입니다.

    const fetchPokemonList = async () => {
      const list = await fetchPokemonDB();
      console.log(list);
    
      return list;
    };
  • fetchPokemonDB는 다음과 같습니다.

    • pokemonAPI.ts 내에 존재
    export const fetchPokemonDB = () => {
      return dbService.collection("pokemonDB").get();
    };
  • get은 querySnapShot을 리턴합니다.

    공식문서 참고https://firebase.google.com/docs/reference/js/v8/firebase.firestore.QuerySnapshot

    image

  • 다음과 같이 사용합니다.

    • map을 사용하는 것이 아니라 forEach만 있으므로 배열을 리턴하지 않음
    • 값을 forEach 안에서 저장해줘야 함

id 추가

  • id가 없거나 있을 때 코드를 어떻게 써야 효율적인지..?
  • id?: string은 너무 근본없어 보이기 때문에 firebase용 DTO를 추가했습니다.
export interface FBPokemonDTO extends PokemonDTO {
  id: string;
}

✨ React-query ✨

2. [react-query] 리스트 관련 query key 추가 및 캐시 관리

  • return타입과 return에 pokemonList 추가

    // Hook return 타입
    interface UsePoketmon {
      getPokemonQuery: (no: number) => Promise<PokemonDTO>;
      updateIdNo: () => void;
      idNo: idNoDTO;
      newPokemon: PokemonDTO;
      pokemonList: FBPokemonDTO[];
    }
    ...
    return { getPokemonQuery, updateIdNo, newPokemon, pokemonList, idNo };
  • react-query devtools에서 작동 확인

image

⛔ 문제상황 : 리스트 리랜더링이 일어나지 않음

  • 포켓몬 리스트가 리랜더링이 일어나지 않음
    • 리액트 쿼리 devtool, 커스텀 훅 내부에서는 값이 바뀌지만
      • 컴포넌트에서 리랜더링이 발생하지 않습니다.
      • newPokemon은 리랜더링이 일어나는데, pokemonList만 리랜더링이 일어나지 않는 이유를 알 수 없었습니다.

image

export default function PokemonList() {
  const { pokemonList, currPokemon: newPokemon } = usePoketmonQuery();
  
  useEffect(() => {
    console.log("데이터를 새로 받아옴");
    console.log(pokemonList);
  }, [idNo.curr, pokemonList]);
  
  if (pokemonList.length === 0) return <></>;
  ...
  • newPokemon, idNo 등 변하고 있는 값을 useEffect에 넣었으나, 리랜더링 발생하지 않습니다..
  • newPokemon 값에 따라 PokemonNew 컴포넌트는 리랜더링 되고 있습니다.
  • ⇒ ??
    • 다음과 같은 똑같은 코드를 컴포넌트에 동일하게 적용했을 때, 특정 컴포넌트에 작동하지 않았습니다.
useEffect(() => {
  console.log(idNo);
}, [idNo]);
  1. src\components\pokemon\pokemon-list\PokemonList.tsx
export default function PokemonList() {
  const { pokemonList, idNo } = usePoketmonQuery();
	
	useEffect(() => {
	  console.log(idNo);
	}, [idNo]); // 동작하지 않음
  1. src\components\pokemon\pokemon-new\PokemonNew.tsx
export default function PokemonNew() {
  const { currPokemon, updateIdNo, idNo } = usePoketmonQuery();

  useEffect(() => {
    console.log(idNo);
  }, [idNo]); // 동작함

커스텀 훅에 대해서

  • 커스텀 훅에 대한 이해도가 낮아서 생긴 트러블 슈팅이었습니다.
    • 두 컴포넌트를 비교해보고, updateIdNo 함수를 실행해 본 후 hook 관리에 문제가 있음을 알게 되었습니다.
    • react-query에서 문제점을 찾으려 하다보니 문제 해결에 시간이 걸렸습니다.
  • 커스텀 훅은 hook 또는 hook에 관련된 값을 리턴하는 컴포넌트 입니다.
  • 커스텀 훅의 hook ⇒ 커스텀 훅을 불러온 곳에 포함
    • 값이 어딘가에 존재하는 것이 아니라, 해당 컴포넌트에 포함되어 있습니다.
    • 따라서 바뀌는 값에 따라 리랜더링 하려면, 부모 컴포넌트에서 커스텀 훅을 불러와야 합니다.
    • 리랜더링은 state값이 바뀌거나, props 전달값이 바뀌어야 발생하기 때문입니다.
해결시간: 3시간
문제원인: 커스텀훅 개념 미숙지
검색방법: react-query 공식문서, chatGPT

추가로 참고할만한 내용 ⇒ 리액트 쿼리 clear와 캐시

  • pokemonList 값이 업데이트되지 않을 수 있는 이유는, setIdNo 함수에서 queryClient.clear() 를 호출하기 때문입니다. queryClient.clear() 함수는 모든 React Query 캐시를 지우는 것으로, pokemonList 요청에 대한 캐시가 지워져 다시 데이터를 가져오지 않고, 초기값인 [] 가 남아있기 때문입니다.

  • 해결 방법으로는 queryClient.clear() 를 호출하지 않고, 데이터베이스에서 데이터를 가져왔을 때 그 데이터를 캐시에 저장하는 것입니다. 즉, 아래와 같이 fetchPokemonList 함수에서 리턴하는 데이터를 React Query 캐시에 저장해주는 것입니다.

    csharpCopy code
    const { data: pokemonList = [], refetch } = useQuery(
      [queryKeys.pokemonList, idNo.curr],
      async () => {
        const list = await fetchPokemonList();
        queryClient.setQueryData([queryKeys.pokemonList, idNo.curr], list);
        return list;
      }
    );
    

데이터 저장하는 시점 변경

  • db를 연결하고 나니, 포켓몬을 뽑았을 때 바로 저장해야 된다는 것을 알게 되었습니다.
    • 화면에서 사라질 때 리스트에 추가되면 되는 client 와는 다르게,
      sever에 저장할 값은 포켓몬을 뽑았을 때 필요함,
      새로고침을 해도 이전 뽑은 내용 저장 되어야 합니다.
  • 관련 내용 수정
    • 현재 페이지의 포켓몬을 저장할 경우, 리랜러딩 되면 계속 저장됨
      • 한 번 저장한 포켓몬을 확인할 방법
        • 같은 이름과 넘버 포켓몬 중복이 있으므로, 이름과 넘버만으로는 구분 x
    • 따라서 “클릭했을 때” 현재 포켓몬을 저장합니다.
      • 그래서 저장하는 “시점”의 현재 포켓몬 + 새로 불러올 포켓몬 : 2가지로 분리
        • currPokemon, newPokemon

        • 새로 가져올 포켓몬 newPokemon

          // useQuery 시작
          const fallback = initPokemon();
          const { data: currPokemon = fallback } = useQuery(
            [queryKeys.pokemon, idNo.curr],
            () => {
              if (idNo.curr === 0) return fallback;
              return getPokemonQuery(idNo.curr);
            }
          );
          
          const { data: newPokemon } = useQuery([queryKeys.pokemon, idNo.next], () => {
            return getPokemonQuery(idNo.next);
          });
        • newPokemon을 저장함

          async function updateIdNo() {
            setIdNo({ curr: idNo.next, next: getId() });
          
            savePokemonDB(newPokemon);
            queryClient.clear();
          }
        • api 함수에 undefined와 예외 내용 추가

          • 에러를 던지면 최초에 값이 0일 때 오류납니다. 따라서 그냥 리턴.
          export const savePokemonDB = async (payload: PokemonDTO | undefined) => {
            if (!payload) return;
          
            try {
              const result = await dbService.collection("pokemonDB").add(payload);
            } catch (err) {
              throw new Error("포켓몬 저장 실패");
            }
          };

✨ VIEW ✨

포켓몬 리스트 state 화면에 보여주기 완료

포켓몬 no 순서대로 나오도록 정렬

/** snapShot을 return합니다. */
const fetchPokemonList = async () => {
  const pokemonDB = await fetchPokemonDB();
  let newList: FBPokemonDTO[] = [];

  pokemonDB.forEach((document) => {
    const object = {
      ...document.data(),
      id: document.id,
    };
    newList.push(object as FBPokemonDTO);
  });
  **newList.sort((a, b) => a.no - b.no);**

  // console.log(newList);
  return newList;
};

작업 완료 코드의 useQuery부분

  • idNo.curr가 바뀔 때 새 값 가져오고, 이전 데이터를 보관 ⇒ 로딩 시 캐시 데이터를 보여줌
const { data: pokemonList = [] } = useQuery(
  [queryKeys.pokemonList, idNo.curr],
  async () => {
    const list = await fetchPokemonList();
    queryClient.setQueryData([queryKeys.pokemonList, idNo.curr], list);
    return list;
  },
  {
    keepPreviousData: true,
  }
);

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.