섹션 3의 막바지에 다다르고 있습니다. 내용이 알차서 그런가 강의 정리가 많네요. 사실 정리하는 것보다 직접 타이핑하면서 왜 이렇게 썼는지 생각하는 시간이 더 긴 것 같습니다. 그리고 새해를 맞이해 드디어 회사에서 반려노트북을 받게 됐는데 삼성 노트북... 생각보다 별론거 같습니다. 거금을 들였는데 아직 일주일도 안됐는데도 끊김이나 버벅임이 좀 있네요ㅜ 무게도 그램보다 무거워서 아쉽습니다만 전면 패널 터치 가능이니 그럴만도 싶습니다. (사실 이 가격이었으면 맥 프로를 사자고 했음 좋았을텐데 회사 분위기상 윈도우가 대부분이라... 슬프네요) 그래도 이번에 새로나온 인텔 cpu와 램 32기가라서 회사 다닐 동안은 열심히 써봐야지 싶습니다.
시작에 앞서 삽질했던 걸..
지금 생각해보면 정말 간단한건데 배열 형식의 정보를 리액트 상에서 뿌려줄 때 map 을 많이 사용합니다... 이때 map은 기본적으로 return이 되는 함수가 아니므로 { } 안에 내용을 구현했다면 꼭꼭.. return 을 해줍시다. 이걸 잠시 잊고 살다가 간만에 구현하려는데 안돼서 뭐지..? 싶었습니다ㅠㅋㅋ
useContext 를 사용한 탭 전환
x 홈페이지를보면 추천탭과 팔로우탭의 내용이 다른 것을 알 수 있습니다. 이 기능을 위해서 일전 포스팅에서 useContext 를 사용한 TabProvider 의 내용에서 Context 로 tab 내용을 가져와서 사용해줍니다.
탭을 바꾸어서 주소가 바뀌지 않는 것을 보아, 클라이언트 상에서 컴포넌트만 변경해주는 것을 확인해줄 수 있습니다. 이 경우엔 추천탭 전용 컴포넌트와 팔로우 전용 컴포넌트로 나누어 탭이 변하는 값을 가져와 내용을 변경해줄 수 있는 별도의 컴포넌트가 필요함을 알 수 있씁니다.
'use client';
import { createContext, useState } from 'react';
export const TabContext = createContext({
tab: 'rec',
setTab: (value: 'rec' | 'fol'): void => {},
});
type Props = { children: React.ReactNode };
export default function TabProvider({ children }: Props) {
const [tab, setTab] = useState('rec');
return (
<TabContext.Provider value={{ tab, setTab }}>
{children}
</TabContext.Provider>
);
}
"use client";
import { useContext } from "react";
import { TabContext } from "./TabProvider";
import PostRecommends from "./PostRecommends";
import FollowingPosts from "./FollowingPosts";
export default function TabDecider() {
const { tab } = useContext(TabContext);
if (tab === "rec") {
return <PostRecommends />;
}
if (tab === "fol") {
return <FollowingPosts />;
}
}
저번에 지정해둔 Context로 간단하게 구현할 수 있습니다. /home 페이지는 서버 컴포넌트므로 훅을 사용할 수 없기에 따로 컴포넌트로 빼줘야 했습니다.
트렌드에 따른 검색 결과
나를 위한 트렌드 구역을 보시면 트렌드를 클릭하여 해당 내용을 확인할 수 있습니다. 이 내용은 쿼리스트링을 사용하여 값을 가져오게 되는데, 이 값과 추천탭 게시글과 팔로우탭 게시글에서 해준 대로 useQuery 를 사용하여 검색 결과 게시글을 구현해줍니다.
import { getSearchResult } from "../_lib/getSearchResult";
import { useQuery } from "@tanstack/react-query";
import Post from "../../_component/Post";
import { Post as IPost } from "@/model/Post";
type Props = {
searchParams: { q: string; f?: string; pf?: string };
};
export default function SearchResult({ searchParams }: Props) {
const { data } = useQuery<
IPost[],
Object,
IPost[],
[_1: string, _2: string, Props["searchParams"]]
>({
queryKey: ["posts", "search", searchParams],
queryFn: getSearchResult,
staleTime: 60 * 1000,
gcTime: 5 * 60 * 1000,
});
return data?.map((post: IPost) => <Post key={post.postId} post={post} />);
}
여기서 알아둬야할 것은 queryFn에 들어가는 함수에는 기본적으로 queryKey가 들어간다고 합니다. 즉 따로 훅을 사용하여 주소창 값을 읽어오지 않아도 searchParams 부분을 통해 getSearchResult 함수에서 읽어들일 수 있는 것이죠.
그리고 useQuery의 타입을 보면 [ ] 안에 여러 타입들이 들어감을 알 수 있습니다. 이를 다이나믹 쿼리키라고 한다고 합니다. 타입스크립트가 약한 저로썬 처음에 좀 당황스러웠네요.
import { Post } from "@/model/Post";
import { QueryFunction } from "@tanstack/query-core";
export const getSearchResult: QueryFunction<
Post[],
[_1: string, _2: string, searchParams: { q: string; pf?: string; f?: string }]
> = async ({ queryKey }) => {
const [_1, _2, searchParams] = queryKey;
const res = await fetch(
`http://localhost:9090/api/search/${
searchParams.q
}?${searchParams.toString()}`,
{
next: {
tags: ["posts", "search", searchParams.q],
},
cache: "no-store",
}
);
if (!res.ok) {
throw new Error("Failed to fetch data");
}
return res.json();
};
tags와 queryKey의 차이점 중 하나는 tags 는 객체 형태로 넣어줄 수 없어서 위처럼 searchParams.q 인 문자열로 넣어줬다는 점입니다.
그리고 알아두셔야할 점은 타입스크립트에서 쿼리함수의 타입을 지정하는 것인데,
import { QueryFunction } from "@tanstack/query-core";
지정된 타입은 이러하며 <> 안에 구현해준 타입들이 들어가게 만들면 됩니다.
그리고 search form 에서 검색 할 때마다 주소를 변경해주는 로직을 짜줍니다.
"use client";
import style from "@/app/(afterLogin)/_component/rightSearch.module.css";
import { useRouter } from "next/navigation";
import { FormEventHandler } from "react";
type Props = {
q?: string;
};
export default function SearchForm({ q }: Props) {
const router = useRouter();
const onSubmit: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
router.push(`/search?q=${e.currentTarget.search.value}`);
};
return (
<form className={style.search} onSubmit={onSubmit}>
<svg width={20} viewBox="0 0 24 24" aria-hidden="true">
<g>
<path d="M10.25 3.75c-3.59 0-6.5 2.91-6.5 6.5s2.91 6.5 6.5 6.5c1.795 0 3.419-.726 4.596-1.904 1.178-1.177 1.904-2.801 1.904-4.596 0-3.59-2.91-6.5-6.5-6.5zm-8.5 6.5c0-4.694 3.806-8.5 8.5-8.5s8.5 3.806 8.5 8.5c0 1.986-.682 3.815-1.824 5.262l4.781 4.781-1.414 1.414-4.781-4.781c-1.447 1.142-3.276 1.824-5.262 1.824-4.694 0-8.5-3.806-8.5-8.5z"></path>
</g>
</svg>
<input name="search" type="search" />
</form>
);
}
currentTarget 을 활용하여 form 태그 안의 name에 지정된 키로 해당 값의 내용에 접근할 수 있습니다. 따로state 를 사용하지 않아도 되므로 간편하게 구현이 가능해집니다.
다만 검색할 때마다 관련 검색params의 결과로 아래와 같이 리액트 쿼리 캐싱에 쌓이게 됩니다. 그러므로 캐싱 시간을 짧게 하거나 특정 조건일 때 정리를 해주는 등의 메모리 관리가 별도로 필요합니다.
추가로 게시글 내용은 MSW 를 사용하여 만든 더미 데이터입니다. 이때 params 로 각 api마다 다이나믹하게 바뀌는 요청주소를 받아줄 수 있는데 넥스트는 폴더명에 [ ] 를 감싼 것에 비하여 MSW에선 변수명에 : 을 붙여서 나타냅니다.
http.get(
"/api/users/:userID/posts/:postId/comments",
async ({ request, params }) => {
const { userID, postId } = params;
//...
)
나머지는 거의 구현된 내용을 바탕으로 내용을 붙여넣기하거나 설정을 고쳐주는 수준으로 처리하면 된다고 합니다.
트렌드 목록
"use client";
import { usePathname } from "next/navigation";
import style from "./trendSection.module.css";
import Trend from "@/app/(afterLogin)/_component/Trend";
import { useSession } from "next-auth/react";
import { useQuery } from "@tanstack/react-query";
import { getTrends } from "../_lib/getTrends";
import { IHashtag } from "@/model/IHashtag";
export default function TrendSection() {
const pathname = usePathname();
const { data: session } = useSession();
const { data } = useQuery<IHashtag[]>({
queryKey: ["trends"],
queryFn: getTrends,
staleTime: 60 * 1000,
gcTime: 5 * 60 * 1000,
enabled: !!session?.user,
});
if (pathname === "/explore") return null;
if (session?.user) {
return (
<div className={style.trendBg}>
<div className={style.trend}>
<h3>나를 위한 트렌드</h3>
{data?.map((trend: IHashtag) => (
<Trend trend={trend} key={trend.tagId} />
))}
</div>
</div>
);
} else {
return (
<div className={style.trendBg}>
<div className={style.noTrend}>트랜드를 가져올 수 없습니다</div>
</div>
);
}
}
트렌드 목록은 로그인 여부에 따라 보이거나 안보여야 합니다. 하지만 useQuery를 enabled 속성을 사용하지 않고 불러온다면 로그인을 하든 안하든 항상 데이터가 불러와지므로 최적화에 좋지 않습니다. 이를 위해 enabled 속성을 사용하여 session 에 user 로그인 정보가 있을 때만 useQuery 를 불러오도록 설정해줄 수 있습니다.
얼레벌레 트렌드 부분과 팔로우 추천 부분을 더미 데이터로 채우면 이렇게 됩니다.
자세한 코드는 아래 깃헙에서 확인 가능합니다.
https://github.com/ZeroCho/next-app-router-z/tree/master/ch3-2
'Web Study > Next.js' 카테고리의 다른 글
Next.js로 SNS x.com 클론코딩하기 - 8.5 (1) | 2024.02.08 |
---|---|
Next.js로 SNS x.com 클론코딩하기 - 8 (2) | 2024.02.07 |
Next.js로 SNS x.com 클론코딩하기 - 6 (2) | 2024.01.30 |
Next.js로 SNS x.com 클론코딩하기 - 5 (1) | 2024.01.25 |
Next.js로 SNS x.com 클론코딩하기 - 4 (1) | 2024.01.24 |