TIL 23. 12. 22

인프런 Next + React Query로 SNS 서비스 만들기 강의와 NextJS 공식문서를 함께 보며 작성

로그인 모달 구현

“X”에서 로그인 버튼을 눌러 로그인 모달이 뜨는데 메인 페이지가 배경으로 남아있는 것을 볼 수 있다. 로그인 모달은 /i/flow/login에 매칭되는 UI이고 배경은 /에 매칭되는 UI이다. image-20231222012037538

병렬 라우트

NextJS에서는 병렬 라우트(Parallel Routes)라는 기능을 제공한다. 병렬 라우트를 사용하면 동일한 레이아웃에서 여러 페이지를 동시에 또는 조건부로 렌더링 할 수 있다.

주의할 점은 병렬 라우트를 적용하기 위해서는 같은 레이어에 위치해야 한다는 것이다. 로그인 모달 @modal.page.tsxpage.tsx의 위치가 같도록 생성한다.

 📂(beforeLogin)
 ┣ 📂@modal
 ┃ ┗ 📜page.tsx
 ┣ 📂i
 ┃ ┗ 📂flow
 ┃ ┃ ┣ 📂login
 ┃ ┃ ┃ ┗ 📜page.tsx
 ┃ ┃ ┗ 📂signup
 ┃ ┃ ┃ ┗ 📜page.tsx
 ┣ 📂login
 ┃ ┗ 📜page.tsx
 ┣ 📜layout.tsx
 ┣ 📜page.module.css
 ┗ 📜page.tsx

병렬 라우트는 정의된 “슬롯”을 사용하여 생성한다. 슬롯을 정의할 때는 @를 붙여서 @이름과 같은 이름 규칙을 사용한다. 폴더명인 @modal은 layout.tsx에서 @ 뒤의 이름(modal)으로 레이아웃의 props가 된다.

// app/(beforeLogin)/layout.tsx

export default function Layout({ children, modal }: LayoutProps) {
  return (
    <div>
      {children}
      {modal}
    </div>
  );
}

이제 로그인 모달 UI를 구현한다.

// app/(beforeLogin)/@modal/page.tsx

import { useState } from 'react';
import style from './login.module.css';

export default function LoginModal() {
  const [id, setId] = useState();
  const [password, setPassword] = useState();
  const [message, setMessage] = useState();

  (...)
}

그런데 평소와 같이 useState 훅을 사용해 상태 관리를 하려고 하자 에러가 발생했다.

🚫 You’re importing a component that needs useState. It only works in a Client Component but none of its parents are marked with “use client”, so they’re Server Components by default.

그 이유는 서버 컴포넌트

app 디렉토리 내부의 모든 컴포넌트는 기본적으로 서버 컴포넌트로 동작한다. 서버 컴포넌트는 서버에서 렌더링 된 후 클라이언트로 전달되고 라우터 수준에서 페이지 이동이 없다면 변경되지 않는다.

서버 컴포넌트는 클라이언트에서 실행되지 않기 때문에 자바스크립트 번들에 코드가 포함되지 않아 번들의 크기를 훨씬 줄일 수 있다. 사용자가 페이지와 완전하게 상호작용할 수 있을 때까지 걸리는 시간(TTI)이 줄어 우수한 사용자 경험을 제공한다.

심지어 리액트 컴포넌트 내에서 데이터베이스 쿼리 작성도 가능하다.

CDN media

하지만 서버 컴포넌트는 렌더링 후 메모리에 유지되지 않아 상태를 가질 수 없다. 따라서 대부분의 훅(hook)을 사용할 수 없다.

또한 이벤트 핸들러 역시 클라이언트에 등록되고 실행되어야 하므로 사용자와의 상호 작용을 추가할 수 없다.

읽을 거리 - 새로 등장한 ‘리액트 서버 컴포넌트 이해하기’

클라이언트 컴포넌트로 전환하기

상태나 이펙트를 사용하는 컴포넌트는 클라이언트에서 실행된다. 따라서, 서버 컴포넌트가 아닌 기존에 사용하던 표준 리액트 컴포넌트를 사용해야 한다. 이 표준 리액트 컴포넌트를 클라이언트 컴포넌트라고 부른다.

서버 컴포넌트를 클라이언트 컴포넌트로 전환하려면 최상단에 "use client" 지시어를 사용한다. "use client" 지시어는 해당 파일이 클라이언트 컴포넌트이며, 클라이언트에서 다시 렌더링 할 수 있도록 JS 번들에 포함해야 한다는 것을 알리는 역할이다.

// app/(beforeLogin)/@modal/page.tsx

"use client";
import { useState } from 'react';
import style from './login.module.css';

export default function LoginModal() {
  const [id, setId] = useState();
  const [password, setPassword] = useState();
  const [message, setMessage] = useState();
  (...)
}

image-20231222122242733

에러 없이 잘 동작한다. 그리고 훅을 사용해 로그인에 필요한 상태를 관리할 수 있게 되었다.

그런데 현재는 / 경로에 메인 페이지와 로그인 모달이 동시에 나타난다. 하지만 실제로는 /i/flow/login 경로에 나타나야 한다.

/i/flow/login 경로에서 메인 화면과 로그인 모달 띄우기

위에서 작성한 @modal/page.tsx@modal/i/flow/login/page.tsx 구조로 변경하자 Not Found 화면이 나타났다.

결론부터 말하자면 (beforeLogin)/layout.tsx 파일에서 modal에 매칭되는 페이지가 없기 때문이다.

// app/(beforeLogin)/layout.tsx

export default function Layout({ children, modal }: LayoutProps) {
  return (
    <div>
      {children}
      {modal}
    </div>
  );
}

아래 디렉토리 구조를 살펴보면 레이아웃은 children으로 같은 레이어의 (beforeLogin)/page.tsx에 정의된 컴포넌트에 해당한다.

📂app
┣ 📂(afterLogin)
┃ ┣ (생략)
┃ ┗ 📜layout.tsx
┣ 📂(beforeLogin)
┃ ┣ 📂@modal
┃ ┃ ┗ 📂i
┃ ┃ ┃ ┗ 📂flow
┃ ┃ ┃ ┃ ┗ 📂login
┃ ┃ ┃ ┃ ┃ ┣ 📜login.module.css
┃ ┃ ┃ ┃ ┃ ┗ 📜page.tsx
┃ ┣ 📂i
┃ ┃ ┗ 📂flow
┃ ┃ ┃ ┣ 📂login
┃ ┃ ┃ ┃ ┗ 📜page.tsx
┃ ┃ ┃ ┗ 📂signup
┃ ┃ ┃ ┃ ┗ 📜page.tsx
┃ ┣ 📂login
┃ ┃ ┗ 📜page.tsx
┃ ┣ 📜layout.tsx
┃ ┣ 📜page.module.css
┃ ┗ 📜page.tsx
┣ 📜layout.tsx
┗ 📜not-found.tsx

app 디렉토리에서 afterLogin과 beforeLogin은 URL에 영향을 주지 않는다고 했다. 그렇다면 afterLogin에는 page.tsx 파일이 없으므로 / 경로에 매칭되는 페이지는 (beforeLogin)/page.tsx이다.

그런데 / 경로에서 modal에 매칭되는 페이지는 없다. @modal/page.tsx 파일이 존재하지 않기 때문에. page.tsx를 생성하면 되지만 사실 / 경로에서는 로그인 모달이 필요가 없다.

병렬 라우트가 필요 없을 때 혹은 병렬 라우트의 기본 값으로 아무것도 표시하지 않을 때 default.tsx를 사용할 수 있다. default.tsx 파일은 병렬 라우트의 대체 페이지이다.

// app/(beforeLogin)/@modal/default.tsx

export default function Default() {
  return null;
}

정리하자면, 현재 주소가 /일 때 (beforeLogin)/layout

  • children(beforeLogin)/page.tsx
  • modal(beforeLogin)/@modal/default.tsx

현재 주소가 /i/flow/login이라면

  • children(beforeLogin)/i/flow/login/page.tsx
  • modal(beforeLogin)/@modal/i/flow/login/page.tsx
/ /i/flow/login
image-20231222154050346 image-20231222161154964

이제 / 에서 로그인 모달이 동시에 나타나지 않는다. /i/flow/login 경로에서는 병렬 라우팅 되어 (beforeLogin)/@modal/i/flow/login/page.tsx(beforeLogin)/i/flow/login/page.tsx가 동시에 렌더링 되고 있다.

마지막, 인터셉트 라우트

이제 한 단계 남았다. /i/flow/login 경로에서 로그인 모달창과 함께 (beforeLogin)/i/flow/login/page.tsx가 아니라 (beforeLogin)/page.tsx가 렌더링 되어야 한다. 인터셉트 라우트를 활용할 때이다.

/i/flow/login이 너무 길어서 이하 (…)으로 대체한다.

인터셉트 라우트는 특정 경로로의 요청이 처리되기 전에 중간에서 가로채어 처리할 수 있도록 하는 기능이다. 이를 활용하여 i/flow/login 경로에 접근하면 (beforeLogin)(...)/page.tsx가 렌더링 되어야 하지만 이를 가로채 (beforeLogin)/@modal/(...)/page.tsx가 렌더링 되도록 처리되도록 한다.

인터셉트 라우트 역시 지켜야 할 규칙이 있다. 인터셉트 경로는 상대 경로 표기법과 같이 (.)으로 정의한다. 이는 파일 시스템 기준이 아니라 ‘브라우저 주소 기준’이다.

  • (.) 같은 레벨의 세그먼트
  • (..) 한 단계 위의 세그먼트
  • (..)(..) 두 단계 위의 세그먼트
📂app
┣ 📂(afterLogin)
┃ ┣ (생략)
┣ 📂(beforeLogin)
┃ ┣ 📂@modal
┃ ┃ ┣ 📂(.)i
┃ ┃ ┃ ┗ 📂flow
┃ ┃ ┃ ┃ ┗ 📂login
┃ ┃ ┃ ┃ ┃ ┣ 📜login.module.css
┃ ┃ ┃ ┃ ┃ ┗ 📜page.tsx
┃ ┃ ┗ 📜default.tsx
┃ ┣ 📂i
┃ ┃ ┗ 📂flow
┃ ┃ ┃ ┣ 📂login
┃ ┃ ┃ ┃ ┗ 📜page.tsx
┃ ┃ ┃ ┗ 📂signup
┃ ┃ ┃ ┃ ┗ 📜page.tsx
┃ ┣ 📂login
┃ ┃ ┗ 📜page.tsx
┃ ┣ 📜layout.tsx
┃ ┣ 📜page.module.css
┃ ┗ 📜page.tsx
┣ 📜layout.tsx
┗ 📜not-found.tsx

(beforeLogin)/@modal/i(beforeLogin)/i는 같은 레벨에 위치한다. 라우트 그룹과 슬롯은 URL에 영향을 주지 않기 때문이다. 로그인 모달에서는 /i/flow/login 경로를 가로채야 하는데, 같은 레벨에 위치해 있으므로 다음과 같이 정의한다. (.)i

동작 방식은 다음과 같다.

  1. 로그인 페이지로 이동하는 버튼을 누른다.

  2. /에서 /i/flow/login 경로로 변경한다.

    2.1. (beforeLogin)/(...)/page.tsx가 렌더링 되어야 하지만 인터셉트 라우트가 이를 가로채 (beforeLogin)/@modal/(...)/page.tsx가 렌더링 된다.

    2.2. 인터셉트 라우트는 동시에 병렬 라우트이다. 따라서 (beforeLogin)/layout.tsx에서 modal로 전달된다.

    2.3. children은 (beforeLogin)/page.tsx, modal은 (beforeLogin)/(...)/page.tsx

  3. 메인 페이지와 로그인 모달이 나타난다.

원래는 /에서 /i/flow/login으로 넘어가면서 /에 매칭되는 페이지가 언마운트 되어야 하지만 인터셉트 라우트가 가로채 처리한다. 사실상 / 경로에 있는 것과 마찬가지이다.

image-20231222162226887

인터셉트 라우팅은 클라이언트에서 라우팅할 때만 적용된다.

그러면 기존의 /i/flow/login 폴더는 의미 없는 게 아닌가?

그렇지 않다. 인터셉트 라우트가 해당 경로를 가로채는 경우는 메인 페이지에서 링크를 통해 URL이 변경될 때이다. 새로고침이나 URL로 직접 접근했을 때는 (beforeLogin)/(...)/page.tsx가 렌더링 된다.

카테고리:

업데이트:

댓글남기기