[TIL] 240702 (Next.js 개인과제 데이터 캐시 처리 / 동적 페이지 metadata 적용)

·2024년 7월 2일

TIL

목록 보기
87/268
post-thumbnail

🥞 오늘 한 일

  • 넥스트 개인과제
    • 선택 구현 사항
      • 포켓몬 리스트 데이터 Tanstack Query 캐시 처리
      • 디테일 페이지 metadata title 적용
      • 부족한 타입스크립트 적용
  • 알고리즘 코드카타
    • 리스트 순서 바꾸기 2

🍽️ 트러블 슈팅

넥스트 개인과제

디테일 페이지 metadata title 적용

과제

선택 구현 사항 중 디테일 페이지에 각기 다른 metadata를 적용시키는 과제가 있었다. 각기 다른 metadata이기 때문에, 현재 페이지의 포켓몬 이름을 title로 적용시켜줄 생각이었다. 때문에 기존 정적 페이지에서 적용되는 방식은 동적 페이지에서는 잘 적용할 수 없었다. 때문에 동적 페이지에서 metadata를 적용하는 방법에 대해서 찾아보았고, 적용시켰다.

해결

import PokemonDetail from "@/components/PokemonDetail";
import axios from "axios";
import React from "react";

import type { Metadata } from "next";

type Props = {
  params: { id: string };
};

// 메타 데이터 설정
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = params;
  const { data: pokemon } = await axios.get<Pokemon>(
    `http://localhost:3000/api/pokemons/${id}`
  );
  return {
    title: `${pokemon.korean_name} | 1세대 포켓몬 도감`,
  };
}

const PokemonDetailPage = async ({
  params,
}: Props): Promise<React.JSX.Element> => {
  const { id } = params;
  const { data: pokemon } = await axios.get<Pokemon>(
    `http://localhost:3000/api/pokemons/${id}`
  );

  return (
    <div>
      <PokemonDetail pokemon={pokemon} />
    </div>
  );
};

export default PokemonDetailPage;

설명을 위해 전체 코드를 가져왔다.
원래는 PokemonDetailPage 내부에서 metadata를 적용시킬 수 있는 방법을 찾아보았다. 그 이유는 params id를 통해 가져온 해당 포켓몬의 이름을 PokemonDetailPage 내부에서만 가져올 수 있기 때문이었다. 그러나 찾아본 방법들 중에서는 그 방법을 찾지 못했고, 결국 generateMetadata 내에서 또 다시 axios를 사용해 포켓몬의 데이터를 가져오는 방식을 사용했다. 그렇게 가져온 데이터로 해당 포켓몬의 이름을 metadata title로 설정해줄 수 있었고, 문제는 해결되었으나 같은 파일에서 두 번이나 똑같은 데이터를 가져와야하는 점이 아쉬운 부분이다.

알고리즘 코드카타

리스트 순서 바꾸기 2

문제

'수정' 버튼을 눌러야 '위로', '아래로', 버튼이 나타납니다.
'1' 과 기대결과는 동일하나, '완료' 버튼 까지 눌려야만 변경된 순서가 반영이 됩니다
'취소' 가 누르면 이전 순서가 그대로 유지가 되어야 합니다.

풀이

풀이는 아래 코드 내에 작성하였다.

import { useState } from "react";
import { MOCK_DATA } from "./MOCK_DATA.js";

const ChangeListOrderAdvanced = () => {
  const [pokemonData, setPokemonData] = useState(MOCK_DATA);
  // 기존 배열을 저장할 새로운 state를 제작한다.
  const [prevPokemonData, setPrevPokemonData] = useState([]);
  const [isEditMode, setIsEditMode] = useState(false);

  // TODO '위로' 버튼을 눌렀을 때, 실행되는 로직을 작성합니다. 첫 번째 아이템은 위로 이동 할 수 없음을 기억해주세요!
  const moveItemUp = (selectedIndex) => {
    // 첫 번째 아이템일 경우 return을 통해 아무 코드도 실행할 수 없게 한다.
    if (selectedIndex === 0) {
      return;
    }
    // 선택된 아이템과 바로 전 아이템을 찾아서 새로운 배열로 만들고, 해당 배열을 reverse한다.
    const filteredPokemons = pokemonData
      .filter((pokemon, index) => {
        return selectedIndex === index || selectedIndex - 1 === index;
      })
      .reverse();
    // 전체 배열을 map 하여, 직전 인덱스에는 필터링한 배열의 첫번째 요소(선택된 아이템)를, 선택된 인덱스에는 두번째 요소(직전 아이템)을 넣는다.
    // 해당 두 경우가 아닐 경우에는 그대로 return 한다.
    // 해당 배열을 pokemonData에 적용한다.
    setPokemonData(
      pokemonData.map((pokemon, index) => {
        if (index === selectedIndex - 1) {
          return filteredPokemons[0];
        } else if (index === selectedIndex) {
          return filteredPokemons[1];
        }
        return pokemon;
      })
    );
  };

  // TODO '아래' 버튼을 눌렀을 때, 실행되는 로직을 작성합니다. 마지막 아이템은 아래로 이동 할 수 없음을 기억해주세요!
  const moveItemDown = (selectedIndex) => {
    // 마지막 아이템일 경우 return을 통해 아무 코드도 실행할 수 없게 한다.
    if (selectedIndex === pokemonData.length - 1) {
      return;
    }
    // 선택된 아이템과 바로 후 아이템을 찾아서 새로운 배열로 만들고, 해당 배열을 reverse한다.
    const filteredPokemons = pokemonData
      .filter((pokemon, index) => {
        return selectedIndex === index || selectedIndex + 1 === index;
      })
      .reverse();
    // 전체 배열을 map 하여, 선택된 인덱스에는 필터링한 배열의 첫번째 요소(직후 아이템)를, 직후 인덱스에는 두번째 요소(선택된 아이템)을 넣는다.
    // 해당 두 경우가 아닐 경우에는 그대로 return 한다.
    // 해당 배열을 pokemonData에 적용한다.
    setPokemonData(
      pokemonData.map((pokemon, index) => {
        if (index === selectedIndex) {
          return filteredPokemons[0];
        } else if (index === selectedIndex + 1) {
          return filteredPokemons[1];
        }
        return pokemon;
      })
    );
  };

  // TODO 변경 완료가 되었을 떄 로직을 작성해 주세요.
  const handleSubmit = () => {
    // 변경 여부 유효성 검사
    if (prevPokemonData === pokemonData) {
      alert("변경 사항이 없습니다.");
      return;
    } else {
      alert("변경이 완료되었습니다.");
    }
    // 수정 상태를 끝내기 위해 isEditMode를 false로 바꾼다.
    setIsEditMode(false);
    // prevPokemonData를 초기화한다.
    setPrevPokemonData([]);
  };

  const toggleEditMode = () => {
    if (!isEditMode) {
      // isEditMode가 아닐 경우(수정 버튼을 클릭하여 수정 상태로 변경되었을 경우) prevPokemonData에 기존 pokemonData를 넣어준다.
      setPrevPokemonData(pokemonData);
    } else {
      // isEditMode일 경우(취소 버튼을 클릭했을 경우) 수정된 부분을 다시 되돌려야 하므로 prevPokemonData에 넣었던 배열을 다시 pokemonData에 넣어준다.
      setPokemonData(prevPokemonData);
      // prevPokemonData를 초기화한다.
      setPrevPokemonData([]);
    }
    // isEditMode가 변경된다.
    setIsEditMode((prevState) => !prevState);
  };

  return (
    <div className="container mx-auto">
      <h2 className="w-full text-center py-10">리스트 순서 바꾸기</h2>
      <div className="flex gap-2 justify-end pb-4">
        {isEditMode ? (
          <>
            {/* TODO 취소가 눌렸을 때 단순히, toggleEdit 을 불러주기 싫을 수도 있을 것 같아요. 마음대로 리팩토링 하셔도 됩니다. */}
            <button
              className="bg-state-error h-10 p-2 rounded text-[#ffffff] font-bold"
              onClick={toggleEditMode}
            >
              취소
            </button>
            {/* TODO 함수에 매개변수로 넣어주고 싶은게 있으시면 추가 시키셔도 됩니다. */}
            <button
              className="bg-section h-10 p-2 rounded text-[#ffffff] font-bold"
              onClick={() => {
                handleSubmit();
              }}
            >
              완료
            </button>
          </>
        ) : (
          <button
            className="bg-brand h-10 p-2 rounded text-[#ffffff] font-bold"
            onClick={toggleEditMode}
          >
            수정
          </button>
        )}
      </div>
      <div className="flex flex-col gap-2">
        {/* TODO Index 도 필요하다면, 수정해주세요 */}
        {pokemonData.map((pokemon, index) => (
          <div
            key={pokemon.id}
            className="pokemon p-4 border rounded-lg flex justify-between"
          >
            <div>
              <img
                src={pokemon.sprites.front_default}
                alt={pokemon.korean_name}
              />
              <p>{pokemon.korean_name}</p>
              <p>도감번호: {pokemon.id}</p>
            </div>
            {isEditMode ? (
              <div className="flex gap-5 items-center">
                {/* TODO moveItemUp 함수에 매개변수로 넣어주고 싶은게 있으시면 추가 시키셔도 됩니다. */}
                <button
                  className="bg-brand h-10 p-2 rounded text-[#ffffff] font-bold"
                  onClick={() => moveItemUp(index)}
                >
                  위로
                </button>
                {/* TODO moveItemDown 함수에 매개변수로 넣어주고 싶은게 있으시면 추가 시키셔도 됩니다. */}
                <button
                  className="bg-state-warning h-10 p-2 rounded text-[#ffffff] font-bold"
                  onClick={() => moveItemDown(index)}
                >
                  아래로
                </button>
              </div>
            ) : null}
          </div>
        ))}
      </div>
    </div>
  );
};

export default ChangeListOrderAdvanced;

🍴 돌아보기

  • 개인과제를 끝내서 약간 마음을 놓았던 것 같다. 나 자신에게 강제로라도 더 과제를 부여하여 더 알차게 시간을 활용해야겠다. 캠프도 막바지다...
  • 타입스크립트에 대한 이해가 좀 부족하다고 생각했다. 원래 무한 스크롤 기능도 제작하려고 했지만 타입 설정에 계속 오류가 나서 일단 보류한 상태이다. 이런 곳에서 발목을 잡히고 싶지는 않기에, 타입스크립트 학습을 조금씩 해나가야겠다.

🍳 내일 목표

  • 넥스트 개인과제
    • 무한 스크롤 혹은 페이지네이션 구현
profile
웹 프론트엔드 개발자

0개의 댓글