Modal 구현
일전의 게시글에서도 한번 설명했다시피 Modal 구현을 복습해보도록 하겠습니다.
이 모달은 메뉴바에 있는 '게시하기' 버튼을 눌렀을 때 나올 모달을 구현할 것이며, 누르면 모달이 뜨면서 주소가 compose/tweet 이 됩니다.
우선 모달을 만들어줄 폴더를 생성해줍니다. @modal 해당 폴더 안에는 기본적으로 default.tsx가 있어야 한다고 했었죠? 그리고 compose/tweet 폴더를 생성해줍니다. 이 안에 모달이 만들어질 예정입니다.
다음으로 모달이 들어갈 layout.tsx에 자리를 마련해줍니다. 이때 @modal 폴더와 구현될 compose 폴더가 동일한 폴더 구조에 있어야 하며, 이 폴더 구조 안에 layout.tsx 또한 구조가 동일해야 합니다. 즉 아래와 같은 구조가 되어야 합니다.
ㄴ (afterLogin)
ㄴ @modal
ㄴ compose
ㄴ tweet
ㄴ page.tsx ------> layout.tsx의 modal
ㄴ default.tsx
ㄴ compose
ㄴ tweet
ㄴ page.tsx ------> 모달 아래의 배경에 해당, layout.tsx의 children
ㄴ layout.tsx
여기까지는 병렬 라우트가 된 것이고, 탐색하기나 쪽지 프로필 등의 메뉴를 이동한 다음에도 동일하게 게시하기 모달이 뜨기 위해선 일전에 로그인 페이지에서 해준 것처럼 인터셉팅 라우팅을 해주면 됩니다.
usePathname
기존에 메뉴바에서 해당 메뉴를 클릭하면 bold 로 변하게 만드는 기능을 했을 때 useSelectedLayoutSegment 훅을 사용했습니다. 클라이언트 컴포넌트로서 주소창의 문자열을 가져와서 판단해주는 훅이었는데 이 방식이 아닌 다른 방법으로 usePathname 훅이 있습니다.
import { usePathname } from 'next/navigation';
해당 훅은 클라이언트 컴포넌트 전용이므로 use client 를 추가해주셔야 합니다.
이 훅도 동일하게 주소창의 문자열을 가져와줍니다.
- useSelectedLayoutSegment() : localhost:3000/explore ---> explore
- usePathname() : localhost:3000/explore ---> /explore
차이점은 슬래시 여부입니다. 쿼리스트링 전까지의 문자열을 반환해줍니다.
searchParams
검색창의 주소창을 보면 호스트네임 뒤에 ? 로 시작하는 문자열이 보입니다. 이는 쿼리스트링이라 부르며, 넥스트에서 쿼리스트링을 가져오려면 searchParams 객체를 사용해주면 되는데 기본적으로 파라미터로 가져올 수 있습니다.
예를 들어 localhost:3000/search?q=강아지&f=멍멍 이런 주소창에서 쿼리 스트링을 가져오고자 한다면,
type Props = {
searchParams: { q: string; f?: string; pf?: string };
};
export default function SearchPage({ searchParams } : Props) {
return <p>{searchParams.q}</p>
}
searchParams 객체에 직접 접근하여 가져올 수 있습니다. 이 때, ?를 사용하여 옵셔널하게 만들어줄 수 있습니다. 이 방식은 서버 컴포넌트에서도 사용될 수 있습니다.
주소 변경
import { useRouter } from 'next/navigation';
import { useSearchParams } from 'next/navigation';
const router = useRouter();
const searchParams = useSearchParams();
x 홈페이지의 검색 창을 보면 인기탭과 최신탭으로 나뉘게 됩니다. 이때 홈 창과는 달리 주소가 변하게 되는데, 해당 기능을 위해 useRouter 와 useSearchParams 를 활용할 수 있습니다. 전의 포스트에서도 말했다시피 router에는 push와 replace 함수가 존재합니다. 그 중 replace 를 사용하여 주소를 옮기되, 쿼리스트링 상 보이는 주소 정보를 가져오기 위해 useSearchParams 훅을 사용해줍니다.
router.replace(`/search?${searchParams.toString()}&f=live`);
게시글 상세 페이지
x 홈페이지의 게시글에 hover 해보면 왼쪽 하단에 주소가 뜨지 않는 것을 알 수 있습니다. 이 경우는 Link 태그를 사용해주지 않은 라우팅 방식으로 볼 수 있으며 이런 것은 클라이언트 딴에서 페이지 변경을 진행한 경우가 많다고 합니다.
이 경우 기존의 Post 컴포넌트는 서버 컴포넌트인데 반해, 게시글을 클릭해주려면 클라이언트 컴포넌트가 되어야 합니다. 서버 컴포넌트를 클라이언트 컴포넌트로 전부 바꾸는건 지양하는 것이 좋으므로 클릭해줘야하는 컴포넌트를 분리시켜 클라이언트 컴포넌트로 만든 다음, 클라이언트 컴포넌트 안에 children 을 사용하여 서버 컴포넌트를 자식으로 두면 된다고 합니다.
<PostArticle post={target}>
<div className={style.postWrapper}>
//...
</div>
</PostArticle>
export default function PostArticle({ children, post }: Props) {
const router = useRouter();
const onClick = () => {
router.push(`${post.User.id}/status/${post.postId}`);
};
return (
<article onClick={onClick} className={style.post}>
{children}
</article>
);
}
그 결과 위 처럼 기존의 하나였던 컴포넌트가 둘로 분리가 되었습니다.
즉, 클라이언트 컴포넌트 안에 서버 컴포넌트를 import 하는 방식으로 사용해주면 안되고, children 이나 Props로 넘겨주면 서버 컴포넌트가 정상적으로 반영된다고 합니다.
캡처링
부모/조상부터 자식/자손으로 핸들러 등의 함수/명령이 동작하는 것, 반대의 의미로는 버블링이 있습니다.
클릭 등의 이벤트를 진행했을 때, 먼저 캡처링이 일어나고 -> 핸들러가 작동 -> 버블링이 일어납니다.
지금 상태에선 프로필 이름에 해당하는 Link태그와 게시글을 누르면 상세 게시글 페이지로 이동하는 이벤트 사이에서 충돌이 일어나 정상적인 작동을 하지 않습니다. 이를 수정해주기 위해선 상세 게시글 페이지로 이동하기 위해 Post 컴포넌트를 감싸준 PostArticle 컴포넌트의 onClick 속성을 onClickCapture 로 변경해줍니다. 그러면 이벤트가 더이상 캡처링 되지 않아 프로필 이름을 클릭하면 프로필 상세 페이지로 이동함을 알 수 있습니다.
다른 방법으론 onClick 이벤트 핸들러 안에 evt.stopPropagation() 을 사용해주면 됩니다. 이 함수는 이벤트가 상위 엘리먼트에 전달되지 않게(버블링 방지) 해줍니다.
evt.preventDefault()
a태그의 페이지 이동, form 태그의 input 을 전송하는 submit 속성 등의 고유 동작을 중단시켜주는 함수
요약하자면 클릭 이벤트와 Link태그 등이 겹치는 경우 원하는대로 작동하지 않는다면 이벤트 캡처링/버블링에 대해 고민해볼 필요가 있습니다.
params
Next.js 는 파일 구조의 라우팅 특징을 가지고 있으므로 라우트가 깊어질수록 폴더 구조가 깊어지게 됩니다. 이때 [ ] 대괄호(슬러그)를 사용한 폴더의 이름만 가져오고자 할 때 사용하는 파라미터이다. searchParams 는 위에 기재했다시피 주소창이 내용을 전부 가져온다면 params 는 동적 라우팅 구조의 주소들을 가져올 수 있습니다.
type Props = {
params: {
username: string;
id: string;
photoId: string;
};
};
export default function PhotoPage({ params }: Props) {}
app
ㄴ [username]
ㄴ status
ㄴ [id]
ㄴ photo
ㄴ [photoId]
위와 같은 구조의 폴더가 있다면, params로 username, id, photoId의 값을 가져올 수 있는 것입니다.
at()
보통 string 이나 array 에서 특정 값에 접근할 때 [ ]와 인덱스를 사용하여 접근합니다. 이때 [ ] 대신 at() 함수를 사용하여 인덱스로 접근하면 동일한 결과가 나타나는데, at의 경우 음수 인덱스에 접근할 때 더욱 편합니다.
const myString = 'hello world';
const lastChar = myString.at(-1); // 'd'
이렇게 레이아웃 부분 클론을 마치고 다음 강의부턴 섹션3으로 들어갑니다. 섹션1~2는 디자이너와 기획자가 준 문서를 기반으로 디자인적인 부분을 만든 것입니다. 섹션3에선 디자인은 완료 되었으나 API가 아직인 상황을 가정으로 하여 진행된다고 합니다. 아래는 반응형까지 마친 레이아웃입니다.
참고: https://github.com/ZeroCho/next-app-router-z/blob/master/ch2-2
'Web Study > Next.js' 카테고리의 다른 글
Next.js로 SNS x.com 클론코딩하기 - 5 (1) | 2024.01.25 |
---|---|
Next.js로 SNS x.com 클론코딩하기 - 4 (1) | 2024.01.24 |
Next.js로 SNS x.com 클론코딩하기 - 2 (1) | 2024.01.22 |
Next.js로 SNS x.com 클론코딩하기 - 1 (0) | 2024.01.18 |
코딩애플 - 마지막 Next.js ... Middleware (0) | 2023.07.09 |