일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- 백준
- 프로그래머스코테
- js코테
- HTML5
- 리액트댓글기능
- 프로그래머스JS
- JS프로그래머스
- 백준js
- 익스프레스
- 리액트커뮤니티
- JS
- 리액트
- 코딩테스트
- 코테
- dp알고리즘
- 몽고DB
- HTML
- 백준nodejs
- 백준구현
- 백준골드
- 프로그래머스
- 포이마웹
- 백준구현문제
- 알고리즘
- 자바스크립트
- css기초
- 백준알고리즘
- CSS
- 안드로이드 스튜디오
- 다이나믹프로그래밍
- Today
- Total
개발새발 로그
NextJS 14 - 레이아웃 본문
useSelectedLayoutSegment로 ActiveLink 만들기
현재 위치에 따라 아이콘이 선택됨을 알 수 있다.
이때 서버 컴포넌트에서는 안되고 클라이언트 컴포넌트에서만 가능하다.
use훅을 사용하면 무조건 클라이언트 컴포넌트를 사용해야한다.
또 onClick도 거의 무조건 클라이언트 컴포넌트를 사용해야한다. -> server Action이라는 게 있는데 아직 실험적인 단계
-> 그래서 보통 컴포넌트로 따로 만들어서 빼준다.
주의점
useSelectedLayoutSegment는 layout.tsx에서 import한 컴포넌트만 쓸 수 있습니다.
page.tsx에서 쓰면 무조건 null이 나옵니다.
다시말해
useSelectedLayoutSegmemt는 layout 페이지에서만 사용 가능합니다.
page.tsx에서는 안 됩니다.
import NavMenu from "@/app/(afterLogin)/_component/NavMenu"; //useSeletedLayoutSegment 사용한 컴포넌트
export default function AfterLoginLayout({ children }: { children: ReactNode }) {
return (
<div className={style.container}>
<header className={style.leftSectionWrapper}>
...
<nav>
<ul>
<NavMenu />
</ul>
<Link href="/compose/tweet" className={style.postButton}>게시하기</Link>
</nav>
...
</header>
...
)
}
layout.tsx에서 사용한 모습이다.
useSelectedLayoutSegment사용법
'use client'
import {useSelectedLayoutSegment} from "next/navigation";
const segment = useSelectedLayoutSegment(); // 현재 최상위 경로 가져옴
이때 만약 경로가 compose/tweet이더라도 segment값은 tweet이 나온다.
만약 자식의 경로까지 필요하다면
import {useSelectedLayoutSegments} from "next/navigation";
const segment = useSelectedLayoutSegments();
useSelectedLayoutSegments로 사용하면 된다.
실제 사용방법
<Link href="/home">
<div className={style.navPill}>
{segment === 'home' ? ... : ... }
useSelectedLayoutSegment와 usePathname 두 훅의 차이점
"useSelectedLayoutSegmemt는 layout 페이지에서만 사용 가능합니다. page.tsx에서는 안 됩니다. usePathname은 다른 곳에서도 사용 가능하고요.
usePathname은 pathname이 통째로 나오므로 별도의 처리가 필요하지만 자기 마음대로 처리할 수 있어 자유도가 높기도 합니다.
/product, /en/product 같은 다국어 처리가 된 페이지에서는 layoutSegment가 product만 뜨므로 이 때는 layoutSegment가 편하겠네요."
dayjs
요즘에는 dayjs가 점점 트렌드가 되고 있다.
https://day.js.org/docs/en/display/from-now#docsNav
npm install dayjs
dayjs의 from now를 사용한다.
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/ko';
dayjs.locale('ko');
dayjs.extend(relativeTime)
...
return (
...
<div className={style.postBody}>
...
<span className={style.postDate}>{dayjs(target.createdAt).fromNow(true)}</span>
</div>
...
dayjs모두 플러그인 방식이라 필요한게 있으면 플러그인을 가져와야 한다.
한글 플러그인도 설정이 가능하다.
현재 게시글이 나의 좋아요가 되어있는지 안되어있는지
classnames를 활용했다.
npm install classnames
실제 사용 예제를 보자
return (
<div className={style.actionButtons}>
<div className={cx(style.commentButton, { [style.commented]: commented }, white && style.white)}>
...
<div className={style.count}>{1 || ''}</div>
</div>
<div className={cx(style.repostButton, reposted && style.reposted, white && style.white)}>
...
<div className={style.count}>{1 || ''}</div>
</div>
<div className={cx([style.heartButton, liked && style.liked, white && style.white])}>
...
<div className={style.count}>{1 || ''}</div>
</div>
</div>
)
...
// css module
.commentButton.white svg, .commentButton.white .count {
fill: white;
color: white;
}
.repostButton.white svg, .repostButton.white .count {
fill: white;
color: white;
}
.heartButton.white svg, .heartButton.white .count {
fill: white;
color: white;
}
.commentButton.commented svg, .commentButton:hover svg {
fill: rgb(29, 155, 240)
}
.commentButton.commented .count, .commentButton:hover .count {
color: rgb(29, 155, 240);
}
...
.repostButton.reposted svg, .repostButton:hover svg {
fill: rgb(11, 175, 123)
}
.repostButton.reposted .count, .repostButton:hover .count {
color: rgb(11, 175, 123)
}
...
.heartButton.liked svg, .heartButton:hover svg {
fill: rgb(228, 34, 126);
}
.heartButton.liked .count, .heartButton:hover .count {
color: rgb(228, 34, 126);
}
모달을 만들때
트위터는 모달을 열때 url이 compose/tweet으로 바뀐다.
그래서 모달을 구현할 때 먼저 실제 compose/tweet 경로의 page.tsx에는 모달 뒤에 띄워질 배경을 만들어주고,
실제 모달을 패러랠 라우트로 @modal/compose/tweet/page.tsx에 만들어준다음
메인 layout에 {modal}로 추가해준다.
이때 modal에는 default.tsx를 꼭 만들어서 return null을 해준다.
그리고 어느 경로에서든 (localhost:3000 루트경로가 아닌) 모달을 여는 버튼을클릭하면
현재 경로 위에 모달을 띄워줘야하기 때문에 인터셉팅 라우트도 해줘야 한다.
마지막으로 새로고침했을 때의 문제는 인터셉팅 라우트를 해주고
새로고침했을 때 경로에서 모달을 떠야하므로 아래와 같이 compose/tweet에 구성해야한다.
import Home from "@/app/(afterLogin)/home/page";
import TweetModal from "@/app/(afterLogin)/@modal/(.)compose/tweet/page";
export default function Page() {
return (
<>
<Home/>
<TweetModal/>
</>
)
}
위처럼 구성하면 모달을 열고 새로고침했을 때 백그라운드에는 Home 페이지가 뜨고
TweetModal이 뜨게 된다.
만약 이미지 input에 아이콘을 넣고 싶을 때?
const imageRef = useRef<HTMLInputElement>(null);
const onChange: ChangeEventHandler<HTMLTextAreaElement> = (e) => {
setContent(e.target.value);
}
const onClickButton = () => {
imageRef.current?.click();
}
<div className={style.footerButtonLeft}>
<input type="file" name="imageFiles" multiple hidden ref={imageRef} onChange={onUpload} />
<button className={style.uploadButton} type="button" onClick={onClickButton}>
<svg width={24} viewBox="0 0 24 24" aria-hidden="true">
<g>
<path
d="M3 5.5C3 4.119 4.119 3 5.5 3h13C19.881 3 21 4.119 21 5.5v13c0 1.381-1.119 2.5-2.5 2.5h-13C4.119 21 3 19.881 3 18.5v-13zM5.5 5c-.276 0-.5.224-.5.5v9.086l3-3 3 3 5-5 3 3V5.5c0-.276-.224-.5-.5-.5h-13zM19 15.414l-3-3-5 5-3-3-3 3V18.5c0 .276.224.5.5.5h13c.276 0 .5-.224.5-.5v-3.086zM9.75 7C8.784 7 8 7.784 8 8.75s.784 1.75 1.75 1.75 1.75-.784 1.75-1.75S10.716 7 9.75 7z"></path>
</g>
</svg>
</button>
</div>
useRef를 이용해서 input에 연결하고
input은 hidden으로 숨겨준다.
이후 button클릭이벤트에 연결해준 ref에 클릭이벤트가 나게하고,
input에는 onchange 이벤트를 만들어준다.
만약 특정 경로에서는 레이아웃에 있는 컴포넌트의 위치가 이동해야된다면?
먼저 usePathname을 사용해서 해당 경로면 null을 리턴해서 레이아웃에서 안보이게 한다.
NextJS는 기본적으로 props에 url를(파라미터) 가져올 수 있다.
이때는 서버 컴포넌트일 때다.
만약 클라리언트 컴포넌트 일 때는
usePathname, useSelectedLayoutSegment, useSelectedLayoutSegments 이런 훅을 사용해야한다.
쿼리 스트링 useSearchParams
값을 읽어오는 메서드
searchParams.get(key) - 특정한 key의 value를 가져오는 메서드, 해당 key 의 value 가 두개라면 제일 먼저 나온 value 만 리턴
searchParams.getAll(key) - 특정 key 에 해당하는 모든 value 를 가져오는 메서드
searchParams.toString() - 쿼리 스트링을 string 형태로 리턴
값을 변경하는 메서드
searchParams.set(key, value) - 인자로 전달한 key 값을 value 로 설정, 기존에 값이 존재했다면 그 값은 삭제됨
searchParams.append(key, value) - 기존 값을 변경하거나 삭제하지 않고 추가하는 방식
serchParams 을 변경하는 메서드로 값을 변경해도 실제 url 의 쿼리 스트링은 변경되지 않습니다.
이를 변경하려면 setSearchParams 에 searchParams 를 인자로 전달해야 합니다.
코드 예시
해당 훅을 호출하고
import { useSearchParams } from 'react-router-dom';
useSearchParams 를 선언하고 setSortParams 함수를 만듭니다. set 메서드를 사용해 sort 라는 키에 clear 라는 밸류를 설정한 후 현재 url 의 쿼리 스트링을 변경합니다.
const [searchParams, setSearchParams] = useSearchParams();
const setSortParams = () => {
searchParams.set('sort', 'clear');
setSearchParams(searchParams);
};
여기서 append 메서드를 사용하면 기존 값을 유지하며 sort 라는 키에 hello-world 라는 밸류를 추가할 수 있습니다.
const appendSortParams = () => {
searchParams.append("sort", "hello-world");
setSearchParams(searchParams);
};
서버컴포넌트와 클라이언트 컴포넌트를 잘 분리하는 방법
예를 들어 현재 서버컴포넌트에서 use훅을 사용해야 될 때가 있다고가정하자
그럼 어쩔 수 없이 클라이언트 컴포넌트를 사용해야하는데
그러지말고
클라이언트 컴포넌트 성격을 가진 컴포넌트는 따로 컴포넌트로 만들어서 해당 컴포넌트 내에서만 클라이언트 컴포넌트로 사용하면된다.
즉 서버 컴포넌트에서 클라이언트 컴포넌트를 import 해서 사용하면된다.
근데 이때 게시글 전체 부분을 클릭했을 때 useRouter을 이용해서 해당 게시글의 url로 이동하는 로직을 만들어야한다고 가정하자
그럼 아래와 같이 서버컴포너트로 할 부분은 그대로 두고
import style from './post.module.css';
import Link from "next/link";
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/ko';
import ActionButtons from "@/app/(afterLogin)/_component/ActionButtons";
import PostArticle from "@/app/(afterLogin)/_component/PostArticle";
import {faker} from '@faker-js/faker';
import PostImages from "@/app/(afterLogin)/_component/PostImages";
dayjs.locale('ko');
dayjs.extend(relativeTime)
type Props = {
noImage?: boolean
}
export default function Post({ noImage }: Props) {
const target = {
postId: 1,
User: {
id: 'elonmusk',
nickname: 'Elon Musk',
image: '/yRsRRjGO.jpg',
},
content: '클론코딩 라이브로 하니 너무 힘들어요 ㅠㅠ',
createdAt: new Date(),
Images: [] as any[],
}
if (Math.random() > 0.5 && !noImage) {
target.Images.push(
{imageId: 1, link: faker.image.urlLoremFlickr()},
{imageId: 2, link: faker.image.urlLoremFlickr()},
{imageId: 3, link: faker.image.urlLoremFlickr()},
{imageId: 4, link: faker.image.urlLoremFlickr()},
)
}
return (
<PostArticle post={target}>
<div className={style.postWrapper}>
<div className={style.postUserSection}>
<Link href={`/${target.User.id}`} className={style.postUserImage}>
<img src={target.User.image} alt={target.User.nickname}/>
<div className={style.postShade}/>
</Link>
</div>
<div className={style.postBody}>
<div className={style.postMeta}>
<Link href={`/${target.User.id}`}>
<span className={style.postUserName}>{target.User.nickname}</span>
<span className={style.postUserId}>@{target.User.id}</span>
·
</Link>
<span className={style.postDate}>{dayjs(target.createdAt).fromNow(true)}</span>
</div>
<div>{target.content}</div>
<div>
<PostImages post={target} />
</div>
<ActionButtons/>
</div>
</div>
</PostArticle>
)
}
PostArticle이라는 컴포넌트를 아래와 같이 구성해주면 서버컴포넌트와 클라이언트 컴포넌트를 효과적을 분리해서 사용할 수 있다.
"use client";
import {ReactNode} from "react";
import style from './post.module.css';
import {useRouter} from "next/navigation";
type Props = {
children: ReactNode,
post: {
postId: number;
content: string,
User: {
id: string,
nickname: string,
image: string,
},
createdAt: Date,
Images: any[],
}
}
export default function PostArticle({ children, post}: Props) {
const router = useRouter();
const onClick = () => {
router.push(`/${post.User.id}/status/${post.postId}`);
}
return (
<article onClickCapture={onClick} className={style.post}>
{children}
</article>
);
}
왜 이렇게 하냐면
클라이언트 컴포넌트에서 서버컴포넌트를 import해서 사용하면
서버컴포넌트는 클라이언트 컴포넌트가 된다.
항상 서버컴포넌트는 클라이언트 컴포넌트의 자식일 때 children이나 props를 이용해준다.
이벤트 캡처링 - 이벤트가 겹칠 때
현재 포스트 전체부분에는 클릭했을 때 게시글 상세페이지로 가능 이벤트가 걸려있다.
하지만 포스트 전체부분 안에는 사용자 이름이 있고, 사용자이름을 클릭하면 사용자 정보페이지로 가야하는데
게시글 상세페이지로 가게된다.
만약 포스트 전체부분을 클릭했을 때는 게시글 상세페이지로 이동하고
사용자 이름을 클릭했을 때는 사용자 정보 페이지로 가야한다면 어떻게 해야할까?
onClcikCapture란?
onClickCapture는 이벤트가 버블링 단계가 아니라 캡처링 단계에서 동작하므로 주의해야 합니다.
이를 통해 이벤트가 실제 대상 요소에 도달하기 전에 부모 요소 등에서 미리 처리할 수 있습니다.
NextJS에서 params
저번에는 searchParams를 사용했었다
NextJS는 params도 props에서 바로 갖고올 수 있다.
위 주소창에서 slug를 사용한 부분들은 params로 갖고올 수 있다.
만약 depth가 깊은 경로에 모달을 만든다면
위와 같은 경로에 패러랠 라우트로 모달을 띄운다면
들어가는 폴더에서 페이지가 있는 곳들은 꼭 default.tsx를 넣어줘야한다.
즉 [username], [id] 에는 꼭 넣어줘야한다.
쉽게말해서 page.tsx를 사용하고 있는 경로에는 default.tsx를 넣어줘야한다.
아니면 오류가 발생할 수 있다.
배열 마지막 요소 가져오는 법(예를 들어 카톡 최근 메시지 보이는 방법)
이미지가 하나일 때와 두개일 때의 동적인 레이아웃
if (!post.Images) return null;
if (!post.Images.length) return null;
if (post.Images.length === 1) {
return (
<Link
href={`/${post.User.id}/status/${post.postId}/photo/${post.Images[0].imageId}`}
className={cx(style.postImageSection, style.oneImage)}
style={{ backgroundImage: `url(${post.Images[0]?.link})`, backgroundSize: 'contain'}}
>
<img src={post.Images[0]?.link} alt="" />
</Link>
)
}
if (post.Images.length === 2) {
return (
...
)
}
if (post.Images.length === 3) {
return (
...
)
}
if (post.Images.length === 4) {
return (
...
)
}
return null;
}
이런식으로 하더라!
트위터는 이미지를 4개로 한정해서 이렇게 가능한 것 같았다.
너무 간단하게 처리했음, 어렵게 생각 x
그리고 backgroundimage에 이미지 url을 주고, img태그도 사용했는데
웹접근성때문에 두가지를 모두 사용한 것 같다고 추측한다.