이제 커뮤니티들이 게시되는 메인페이지를 생성해보자.
✅ 파일 생성
// navBar.tsx
const NavBar = () => {
const { loading, authenticated } = useAuthState();
...
return (
<div>
<span>
<Link href="/">
<a>
<Image alt="logo" width={80} height={45}></Image>
</a>
</Link>
</span>
<div>
<div>
<FaSearch/>
<input type="text" placeholder="Search Reddit"/>
</div>
</div>
<div>
{!loading &&
(authenticated ? (
<button onClick={handleLogout}>로그아웃</button>
) : (
<>
<Link href="/login">
<a>로그인</a>
</Link>
<Link href="/register">
<a>회원가입</a>
</Link>
</>
))}
</div>
</div>
);
};
export default NavBar;
✅ 로그인과 회원가입 페이지에서는 Navigation Bar가 필요하지 않으므로 그에 맞는 설정을 해준다.
1️⃣ 만약 현재 페이지가 register
, login
이라면 authRoute
는 true
가 될 것이고, 그렇지 않다면 false
가 될 것이다.
💡 useRouter란?
2️⃣ authRout가 true라면 즉, 로그인,회원가입 페이지라면 Navbar을 보이지 않게, 아니라면 Navbar을 보이게 한다.
3️⃣ Navbar의 존재에 따라 컴포넌트가 위아래로 움직이는 것을 막기 위해 paddingTop을 준다.
💡 pageProps?
// _app.tsx
export default function App({ Component, pageProps }: AppProps) {
...
// 1️⃣ 번
const { pathname } = useRouter();
const authRoutes = ['/register', '/login'];
const authRoute = authRoutes.includes(pathname);
return (
<AuthProvider>
{!authRoute && <NavBar />} // 2️⃣ 번
<div className={authRoute ? '' : 'pt-12'}> // 3️⃣ 번
<Component {...pageProps} />
</div>
</AuthProvider>
);
}
로그인시에는 Navbar에 로그아웃이라는 버튼이 나타날 것이다. 이제 로그아웃버튼을 눌렀을 때 실제로 로그아웃 기능이 동작하도록 구현해보자.
✅ handleLogout(로그아웃버튼클릭)
실행시 api요청 후 결과값이 전달되면 LOGOUT이라는 액션이 동작하도록 dispatch. 그리고 화면 리로딩
// navBar.tsx
...
const dispatch = useAuthDispatch();
const handleLogout = () => {
axios
.post('/auth/logout')
.then(() => {
dispatch('LOGOUT');
window.location.reload();
})
.catch(error => {
console.log(error);
});
};
client에서 로그아웃에 대한 api 요청을 하였으므로 그에 대한 response을 보내주면 된다. 로그아웃을 하는 것은 간단하다. 로그인은 토큰에 의해 인증처리가 되었던 것임으로 로그아웃 버튼 클릭시 토큰을 없애주면 된다.
expires: new Date(0)
: 만료 날짜를 0으로 하여 즉시 토큰 인증이 만료되게 한다src/routes/auth.ts
const logout = async (_: Request, res: Response) => {
res.set(
'Set-Cookie',
cookie.serialize('token', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
expires: new Date(0),
path: '/',
})
);
res.status(200).json({ success: true });
};
const router = Router();
...
router.post('/logout', userMiddleware, authMiddleware, logout);
이제 작성한 커뮤니티들이 등록되어 나타나지는 커뮤니티 목록을 생성해보자!
// index.tsx
const Home: NextPage = () => {
return (
<div>
{/* 포스트 리스트 */}
<div></div>
{/* 사이드바 */}
<div>
<div>
<div>
<p>상위 커뮤니티</p>
</div>
{/* 커뮤니티 리스트 */}
<div></div>
<Link href="/subs/create"> 커뮤니티 만들기</Link>
</div>
</div>
</div>
);
};
작성한 커뮤니티들을 이제 리스트에 가져와서 보여주는 것을 해야하는데, 데이터를 가져오기 위한 React Hooks인 SWR(stale-while-revalidate)
을 사용해보자.
✅ 설치
//client
npm install swr --save
✅ api 요청 (client)
1️⃣ 비동기통신 라이브러리인 axios를 작성한 비동기함수를 fetcher에 저장.
2️⃣ API URL을 address에 저장. (topSubs라는 핸들러가 백엔드에 존재)
3️⃣ useSWR hook은 key 문자열
과 fetcher 함수
를 받는다.
// index.tsx
const Home: NextPage = () => {
const fetcher = async (url: string) => {
return await axios.get(url).then(res => res.data);
};
const address = 'http://localhost:4000/api/subs/sub/topSubs';
const { data: topSubs } = useSWR<Sub[]>(address, fetcher);
💡 type 지정
export interface Sub { createdAt: string; updatedAt: string; name: string; title: string; description: string; imageUrn: string; bannerUrn: string; username: string; posts: Post[]; postCount?: string; imageUrl: string; bannerUrl: string; } export interface Post { identifier: string; title: string; slug: string; body: string; subName: string; username: string; createdAt: string; updatedAt: string; sub?: Sub; url: string; userVote?: number; votesScore?: number; commentCount?: number; }
✅ topSubs 핸들러 (server)
원래는 다음과 같이 쿼리를 작성한다.
하지만,QueryBuilder
는 TypeORM
의 가장 강력한 기능중 하나로, QueryBuilder
는 우아하고 편리한 문법으로 SQL Query
를 생성하고 실행한 다음 자동적으로 변형된 Entity
를 반환해줌으로 다음과 같이 작성해보자.
// subs.ts
const topSubs = async (_: Request, res: Response) => {
try {
// COALESCE: 처음으로 NULL이 아닌 컬럼값을 만나면 그 컬럼 값을 리턴
const imageUrlExp = `COALESCE('${process.env.APP_URL}/images/'
||s."imageUrn",'https://www.gravatar.com/avatar?d=mp&f=y')`;
// createQueryBuilder를 사용해서 Query문을 사용
const subs = await AppDataSource.createQueryBuilder()
.select//테이블에 있는 데이터(제목,이름,이미지,id)를 조회
`s.title, s.name, ${imageUrlExp} as "imageUrl", count(p.id) as "postCount"`)
.from(Sub, 's') // Sub 테이블에서 데이터를 조회
.leftJoin(Post, 'p', `s.name = p."subName"`)//왼쪽 테이블을 중심으로 오른쪽의 테이블을 매치
.groupBy('s.title, s.name, "imageUrl"')// 유형별로 갯수를 조회(데이터를 그룹화)
.orderBy(`"postCount"`, 'DESC') // 데이터 내림차순 정렬
.limit(5) // 5개까지만 데이터 조회
.execute(); // EXECUTE: 준비된 SQL문을 실행
return res.json(subs); // 데이터들을 JSON 데이터로 client에 return(전달)
} catch (error) {
console.log(error);
return res.status(500).json({ error: '문제가 발생했습니다.' });
}
};
router.post('/sub/topSubs', topSubs);
💡 www.gravatar.com
- 이미지 업로드 생략시 기본적으로 나오는 이미지를 불러오는 url로, 이미지를 사용하기위해 설정을 해주자.
// next.config.js const nextConfig = { reactStrictMode: true, swcMinify: true, images: { domains: ['www.gravatar.com', 'localhost'], }, }; module.exports = nextConfig;
💡 잠깐) SWR?
데이터를 가져오기 위한 React Hook 라이브러리.
SWR은 원경 데이터를 가져올 때 캐싱된 데이터가 있으면 그 데이터를 먼저 반환(stale)한 다음 가져오기 요청(revaildate)을 보내고, 마지막으로 최신 데이터와 함께 제공하는 라이브러리이다.
- SWR 특징 및 장점
사용법
useSWR로 React Hook으로, 주된 인자로 key와 fetcher가 있다. 첫 번째 인자는 API URL이면서 캐싱할 때 사용되는 key가 된다. 이는 useSWR('/api/user/123’, fetcher)를 여러 컴포넌트에서 사용하여도 같은 key의 데이터가 있다면 캐싱된 것을 가져오는 것을 말한다.
두 번째 인자는 fetcher이다. Fetch API를 기본으로 하며, 제일 많이 사용되는 Axios 나 GraphQL을 사용할 수 있다.
👉 참고하자!
1️⃣ 앞서 본 topSubs 핸들러를 통해 테이블의 데이터가 client로 전달되었고, 그 값이 data: topSubs
에 저장
2️⃣ 전달받은 데이터들을 map
을 통해 출력하고, 각각 데이터(커뮤니티)들마다 커뮤니티 이름을 가진 고유의 url을 가지게 된다.
const Home: NextPage = () => {
const { data: topSubs } = useSWR<Sub[]>(address, fetcher); // 1️⃣ 번
return (
<div>
...
// 커뮤니티 리스트
<div>
{topSubs?.map((sub) => ( // 2️⃣ 번
<div key={sub.name}>
<Link href={`/p/${sub.name}`}>
<Image
src={sub.imageUrl}
alt="Sub"
width={24}
height={24}
/>
</Link>
<Link href={`/p/${sub.name}`}>
{sub.name}
</Link>
<p>{sub.postCount}</p>
</div>
))}
</div>
</div>
);
};
커뮤니티 생성은 로그인시에만 가능함으로 커뮤니티 생성버튼이 로그인시에만 작동하도록 설정.
const Home: NextPage = () => {
const { authenticated } = useAuthState();
return(
{authenticated && (
<div className="w-full py-6 text-center">
<Link href="/subs/create">
커뮤니티 만들기
</Link>
</div>
)}
)
}