무한스크롤 정리 1
무한스크롤....
나를괴롭혔던무한스크롤,...
사실 언젠가는 ㅋㅋ 해야할 것 같은,,,, 넣어야 할 것 같은 기능이긴 했다.
언제까지 머 일부만 불러오거나 무식하게 한꺼번에 불러오거나 할 순 없으니께...
단지 그게 지금은 아니라고 생각했을 뿐!!!!1!!!!!!!!
근데 결국 넣어야 할 것 같아서 넣었다. . . ㅡㅠ
블로그나 이런 델 열심히 뒤져서 찾아보긴 했는데
쉽다면서요?
...
이해 1나도 안 가...
그래서 정리한다...
먼저 api 불러오는 코드는 내가 짠 것이 아님 그래서 이것도 잘 모른다.
그래서 기존 코드부터 정리해본다...
첫번째 기존 코드
import { NextResponse } from 'next/server';
import axios from 'axios';
const TOTAL_POKEMON = 50;
export const GET = async (request: Request) => {
try {
const allPokemonPromises = Array.from({ length: TOTAL_POKEMON }, (_, index) => {
const id = index + 1;
return Promise.all([
axios.get(`https://pokeapi.co/api/v2/pokemon/${id}`),
axios.get(`https://pokeapi.co/api/v2/pokemon-species/${id}`)
]);
});
const allPokemonResponses = await Promise.all(allPokemonPromises);
const firstStagePokemons = allPokemonResponses
.filter(([pokemonResponse, speciesResponse]) => speciesResponse.data.evolves_from_species === null)
.map(([pokemonResponse, speciesResponse]) => {
const koreanName = speciesResponse.data.names.find((name: any) => name.language.name === 'ko');
return {
...pokemonResponse.data,
korean_name: koreanName?.name || null
};
});
return NextResponse.json(firstStagePokemons);
} catch (error) {
console.error('Error fetching first stage pokemons:', error);
return NextResponse.json({ error: 'Failed to fetch data' });
}
};
사실 이것도 기존 코드는 아니다.
진화 단계가 1단계인 녀석들만 불러오는 코드를 추가해놓은 상태라...
// 우리가 가져올 포켓몬의 수
const TOTAL_POKEMON = 50;
// 누군가 우리 프로그램에 포켓몬 정보를 달라고 하면 이렇게 할 것이다
export const GET = async (request: Request) => {
try {
// 50마리의 포켓몬 정보를 한 번에 가져오려고 준비
// Promise.all은 여러 가지 일을 한꺼번에 시작하고 모든 일이 끝날 때까지 기다리는 방법
const allPokemonPromises = Array.from({ length: TOTAL_POKEMON }, (_, index) => {
//길이가 TOTAL_POKEMON인 배열을 만들기 시작한다.
// 정확히 인덱스는 0부터 49까지의 숫자를 가진다.
Array.from은 배열을 만드는 방법 중 하나다.
이걸 사용하면 숫자, 문자, 객체 같은 여러가지 값을 배열로 쉽게 만들 수 있다.
두 개의 인자를 받는데 현재 요소의 값과, 현재 요소의 인덱스를 받는다.
// 포켓몬 번호는 1부터 시작하니 index에 1을 추가해 id를 만든다.
const id = index + 1;
// 여러 개의 작업을 동시에 시작하고 모두 끝날 때까지 기다린다.
return Promise.all([
axios.get(`https://pokeapi.co/api/v2/pokemon/${id}`),
axios.get(`https://pokeapi.co/api/v2/pokemon-species/${id}`)
]);
});
// 일이 끝날 때까지 기다리라고 시켰다가, 정보를 가져오면 그 결과를 저장한다.
const allPokemonResponses = await Promise.all(allPokemonPromises);
// 가져온 포켓몬 중에서 아직 진화하지 않은 포켓몬만 골라내고,
// 한국어 이름도 추가해서 새로운 목록을 만든다.
const firstStagePokemons = allPokemonResponses
// 진화하지 않은 포켓몬만 골라낸다.
.filter(([pokemonResponse, speciesResponse]) => speciesResponse.data.evolves_from_species === null)
// 골라낸 포켓몬의 정보를 정리한다. 저건 배열의 각 항목이 두 개의 데이터를 포함하고 있다는 의미다.
.map(([pokemonResponse, speciesResponse]) => {
// 포켓몬의 한국어 이름을 찾는다.
const koreanName = speciesResponse.data.names.find((name: any) => name.language.name === 'ko');
// 포켓몬 정보에 한국어 이름을 추가한다.
return {
...pokemonResponse.data,
korean_name: koreanName?.name || null
};
});
// 정리한 포켓몬 정보를 돌려준다.
return NextResponse.json(firstStagePokemons);
} catch (error) {
// 만약 뭔가 잘못되면, 무엇이 잘못됐는지 기록하고
console.error('포켓몬 정보를 가져오다가 문제가 생겼어요:', error);
// 문제가 생겼다고 알려준다.
return NextResponse.json({ error: '정보를 가져오지 못했어요' });
}
};
여기서 NextResponse는 next.js에서 제공하는 응답 객체라고 한다.
클라이언트에게 어떻게 응답할지를 정의한다는데...
뭔지모름
하여튼 저 데이터를 json형식으로 변환해서 그 데이터를 요청한 사람에게 보내는 과정인가보다.
정리하자면 서버가 클라이언트에게 데이터를 응답하는 부분이라
서버가 어떤 데이터를 만들어서 그 데이터를 요청한 사람에게 보낸다는 뜻인 듯.
두번째 코드
import { NextResponse } from 'next/server';
import axios from 'axios';
const LIMIT_POKEMON = 10; // 한 페이지당 보여줄 포켓몬 수
export const GET = async (request: Request) => {
try {
const urlParams = new URL(request.url);
const page = parseInt(urlParams.searchParams.get('page') || '1', 10);
const offset = (page - 1) * LIMIT_POKEMON;
const allPokemonPromises = Array.from({ length: LIMIT_POKEMON }, (_, index) => {
const id = offset + index + 1; // offset을 적용
return Promise.all([
axios.get(`https://pokeapi.co/api/v2/pokemon/${id}`),
axios.get(`https://pokeapi.co/api/v2/pokemon-species/${id}`)
]);
});
const allPokemonResponses = await Promise.all(allPokemonPromises);
const firstStagePokemons = allPokemonResponses
.filter(([pokemonResponse, speciesResponse]) => speciesResponse.data.evolves_from_species === null)
.map(([pokemonResponse, speciesResponse]) => {
const koreanName = speciesResponse.data.names.find((name: any) => name.language.name === 'ko');
return {
...pokemonResponse.data,
korean_name: koreanName?.name || null
};
});
const nextPage = firstStagePokemons.length === LIMIT_POKEMON ? page + 1 : null;
return NextResponse.json({
result: firstStagePokemons,
nextPage,
isLast: nextPage === null
});
} catch (error) {
console.error('Error fetching first stage pokemons:', error);
return NextResponse.json({ error: 'Failed to fetch data' });
}
};
두번째 코드부터 좀 달라지는데,
두번째 코드는 한 페이지당 10마리의 포켓몬을 가져오며
페이지 번호와 오프셋을 계산해 포켓몬의 시작 위치를 설정한다.
또, 다음 페이지와 마지막 페이지 여부를 계산해 클라이언트에게 정보를 돌려준다.
const LIMIT_POKEMON = 10; // 한 페이지당 보여줄 포켓몬 수
export const GET = async (request: Request) => {
try {
const urlParams = new URL(request.url);
const page = parseInt(urlParams.searchParams.get('page') || '1', 10);
const offset = (page - 1) * LIMIT_POKEMON;
const allPokemonPromises = Array.from({ length: LIMIT_POKEMON }, (_, index) => {
const id = offset + index + 1; // offset을 적용
return Promise.all([
axios.get(`https://pokeapi.co/api/v2/pokemon/${id}`),
axios.get(`https://pokeapi.co/api/v2/pokemon-species/${id}`)
]);
});
urlParams는 요청 url에서 정보를 추출하는 객체다.
무한 스크롤에서는 사용자가 어떤 페이지를 보고 있는지를 서버가 알아야
다음에 어떤 데이터를 불러와야 하는지 알 수 있다.
이 정보를 url에서 가져오는 것...
그 과정이 urlParams를 통해 이루어진다.
new url(request.url) 코드는 request.url(클라이언트가 요청한 전체 url)을 분석해 url 객체를 만들어준다.
이 객체는 url에서 다양한 정보를 꺼내올 수 있도록 도와준다.
이쯤되면 또 ㅋㅋ 헷갈린다...
그래서 다시 정리.
무한 스크롤을 구현할 때 우리는 페이지 번호를 알아야 한다.
어떤 페이지를 보고있는지 알아야 하기 때문에.
그래서 페이지 번호를 또 꺼내와야 한다.
const urlParams = new URL(request.url);
const page = parseInt(urlParams.searchParams.get('page') || '1', 10);
이것은 아까 url 객체에서 다양한 정보를 뺄 수 있다고 했다.
여기서 urlParams.searchParams.get('page') 코드를 쓰면
url에서 page 매개변수를 찾아 그 값을 가져온다.
이게 바로 페이지 번호를 꺼내오는 방법이다.
만약 url에 page 매개변수가 없으면 기본값으로 1을 사용한다.
이제 url에서 가져온 값은 문자열이므로 숫자로 변환해주고,
맨 뒤에 10은 parseInt 함수의 두번째 인자로, 숫자의 진수를 나타낸다.
기본적으로 우리가 사용하는 숫자는 10진수므로 10을 걍 써준 것임.
const offset = (page - 1) * LIMIT_POKEMON;
오프셋은 어디서부터 시작할지를 정하는 숫자다.
예를 들어 포켓몬이 100마리 있고, 한번에 10마리씩 보여준다면
페이지별로 어떤 포켓몬부터 보여줄지 정해야 한다.
각 페이지마다 시작할 포켓몬 번호가 다를 것이므로 이를 오프셋으로 계산한다.
페이지 1은 1~10번 포켓몬, 페이지 2는 11~20번 포켓몬까지... 뭐 이런 식으로 한다면
page는 현재 페이지 번호다. 페이지 1일 때는 1번 포켓몬부터 보여줘야 하는데
페이지 1은 첫 페이지므로 오프셋은 0이 되어야 한다.
아이디랑은 다른 게 몇 번 째 항목부터 시작할지를 정하는 것임.
= 오프셋 (데이터 시작 위치(0부터 계산)), id(포켓몬 고유 번호, 1부터 시작)
그래서 현재 페이지에서 1을 뺀 다음에 한 페이지에 보여줄 포켓몬의 수를 곱해주면 1 페이지의 오프셋은 0이다.
그 다음 페이지는 10이 될테고...
이제 머... 그 밑 코드는 위랑 같고
const id = offset + index + 1; // offset을 적용
이 부분의 기존 코드는 index+1로 계산해 1~50까지의 포켓몬을 가져왔다.
지금 코드는 offset + index + 1로 계산한다.
첫번째 페이지의 오프셋은 0이므로 0 + index(0) + 1로 계산해 1부터 10까지의 포켓몬 아이디를 가져올거고
그다음은 11부터 20까지 가져올 것이다.
** 각 페이지마다 오프셋을 이용해 시작 위치를 계산하고, 그 위치부터 10마리의 포켓몬을 가져옴.
const nextPage = firstStagePokemons.length === LIMIT_POKEMON ? page + 1 : null;
return NextResponse.json({
result: firstStagePokemons,
nextPage,
isLast: nextPage === null
});
}
이제 다음 페이지 및 마지막 페이지 여부를 정해줘야 한다.
기존 코드는 단순히 가져온 포켓몬 데이터를 반환만 해줬다.
nextPage는 firstStagePokemons 배열의 길이가 10과 같은지 확인한다.
이 조건이 true면 현재 페이지에 포켓몬이 가득 찼다는 의미고, page + 1을 해준다.
false면 더 이상 포켓몬이 없다는 뜻이므 null을 반환한다.
이제 클라이언트에게 json 형식으로 응답 반환하는 건 같고,
result는 클라이언트에게 실제 데이터를 전달하는 역할을 하는데,
firstStagePokemons 배열을 포함한다.
nextPage는 nextPage 변수를 포함하는데, 이는 다음 페이지 번호를 나타낸다.
isLast는 nextPage === null의 결과를 포함한다. 이 값은 현재 페이지가 마지막 페이지인지 여부를 나타낸다.
근데 이 코드의 문제는 데이터가 딸랑 4개만 나왔다.
아니 왜?,... 뭐가 문젠디. 그래서 다시 수정...
세번째 코드
import { NextResponse } from 'next/server';
import axios from 'axios';
const LIMIT_POKEMON = 10; // 한 페이지당 보여줄 포켓몬 수
export const GET = async (request: Request) => {
try {
const urlParams = new URL(request.url);
const page = parseInt(urlParams.searchParams.get('page') || '1', 10);
const offset = (page - 1) * LIMIT_POKEMON;
const allPokemonPromises = Array.from({ length: LIMIT_POKEMON }, (_, index) => {
const id = offset + index + 1; // offset을 적용
return Promise.all([
axios.get(`https://pokeapi.co/api/v2/pokemon/${id}`),
axios.get(`https://pokeapi.co/api/v2/pokemon-species/${id}`)
]);
});
const allPokemonResponses = await Promise.all(allPokemonPromises);
const firstStagePokemons = allPokemonResponses
.filter(([pokemonResponse, speciesResponse]) => speciesResponse.data.evolves_from_species === null)
.map(([pokemonResponse, speciesResponse]) => {
const koreanName = speciesResponse.data.names.find((name: any) => name.language.name === 'ko');
return {
...pokemonResponse.data,
korean_name: koreanName?.name || null
};
});
const nextPage = page + 1;
return NextResponse.json({
result: firstStagePokemons,
nextPage,
isLast: firstStagePokemons.length < LIMIT_POKEMON
});
} catch (error) {
console.error('Error fetching first stage pokemons:', error);
return NextResponse.json({ error: 'Failed to fetch data' });
}
};
두번째 코드, 세번째 코드의 차이점은 먼저
const nextPage = firstStagePokemons.length === LIMIT_POKEMON ? page + 1 : null;
return NextResponse.json({
result: firstStagePokemons,
nextPage,
isLast: nextPage === null
});
이 부분이다.
두번째 코드에서는 10마리씩 한 페이지에 있다고 가정하고,
다음 페이지를 정할 때 필터링된 포켓몬 수가 정확히 10마리인 경우에만 다음 페이지가 있다고 판단한다.
그래서 필터링된 포켓몬이 10마리가 안 되면 다음 페이지가 없다고 판단해서 넘어가지 않는다.
이 조건으로 마지막 페이지 여부를 판단하는데...
난,,, 잊고 있었다,,,
진화하지 않은 포켓몬만 필터링해서 가져오고 있었다는 것을...
1페이지에서 10마리의 포켓몬을 가져오는데, 그 중에서 진화하지 않은 포켓몬이 4마리면 필터링된 포켓몬 수가
4마리이므로 nextPage는 null이 된다. . .
그래서 해결책으로
const nextPage = page + 1;
return NextResponse.json({
result: firstStagePokemons,
nextPage,
isLast: firstStagePokemons.length < LIMIT_POKEMON
});
항상 다음 페이지를 정한다. 그럼 계속 데이터를 요청할 수 있다.
페이지 번호가 1이면 다음 페이지는 2고, 2면 3이고...
그리고 마지막 페이지 여부를 필터링된 포켓몬 수가 10마리보다 적은지로 판단한다.
여기서 또 헷갈림...
그러니까 두번째 코드는 포켓몬이 정확히 10마리 있어야 다음 페이지가 있다고 생각한다.
그래서 마지막 페이지를 알리는 기능은 있지만 더이상 페이지를 요청하지 않는 것이다.
세번째 코드는 항상 다음 페이지로 넘어간다.
그래서 마지막 페이지 여부를 알리긴 하는데 계속 페이지를 요청할 수 있다.