
병렬 랜더링 패러다임
병렬 랜더링은 화면을 구성하는 요소들을 마치 레고 블록과 같은 독립적인 컴포넌트로 쪼개 개발합니다.

그리고, 최종적으로 하나의 화면에서 동시에 랜더링해내는 현대적인 웹 개발 설계 방식입니다.

이 기술이 왜 선택이 아닌 필수가 되었는지 과거의 웹 화면을 떠올려보며, 치명적인 필요성을 느껴보겠습니다.
하나의 거대한 대시보드 화면이 있다고 가정해보겠습니다.
화면 좌측에 사이드 바 네비게이션, 중앙에는 복잡한 통계 그래프가, 우측에는 실시간 유저 피드와 공지사항이 있습니다.

과거의 모놀리식 즉 모든 코드와 기능이 얽혀있는 거대한 단일 덩어리 아키텍쳐에서는
이 거대한 화면 전체가 오직 하나의 생명체처럼 랜더링되었습니다.

이 방식에는 시스템을 붕괴시킬 수 있는 아주 끔찍한 설계 상의 결함이 존재합니다.
만약 중앙의 통계 그래프를 불러오는데, 데이터베이스 연산이 오래걸려 5초의 지연이 발생한다고 해봅시다.
그래프와는 아무런 논리적 상관도 없는 우측의 공지사항과 좌측의 네비게이션까지 화면 전체가 5초동안 멈춥니다.

또 다른 예시로, 오른쪽의 실시간 피드 영역에서 사소한 자바스크립트 오타로 인해 에러가 발생했다면
그 작은 에러 하나 때문에, 멀쩡한 통계 그래프와 사이드바까지 모조리 붕괴하며
화면 전체가 에러 페이지로 변해버립니다.

이러한 경직된 시스템을 용납해서는 안되며, 시스템의 덩치가 커질수록
작은 오류는 필연적으로 발생하고, 우리는 이를 격리하고 통제할 아키텍쳐가 필요합니다.

이 문제를 우아하게 해결하기 위해 현대 프론트엔드 생태계는
거대한 화면을 통째로 그리려는 욕심을 버리고, 화면을 독립된 여러 개의 구역, 서로 영향을 주지 않는
독립된 공간인 Slot으로 잘게 쪼개는 아키텍처를 도입했습니다.
이 아키텍처 안에서 이들의 각 작업공간은 완벽하게 분리됩니다.
이 분리는 시스템의 안정성 뿐만 아니라, 여러 개발자가 서로의 코드에 간섭받지 않고
동시에 작업할 수 있는 생산성의 향상을 가져옵니다.
이제 통계 그래프 구역에서 데이터를 불러오는데 5초가 걸린다고 해도 나머지 구역은 즉각적으로 화면에 나타납니다.

부분의 실패가 전체적인 실패로 이어지지 않도록 격리하고, 각 기능이 독립적인 가치를 창출하도록 성능을 보장합니다.
이것이 바로 거대한 문제를 작게 나누어 완벽하게 조립해내는 논리적 사고입니다.
병렬 라우트(Parallel Route)
Next.js 16에서는 병렬 랜더링 패러다임을 직관적으로 사용할 수 있도록
병렬 라우트(Parallel Route)라는 강력한 무기를 제공하며, 폴더 이름 앞에 @를 붙이면
프로젝트가 알아서 그 폴더를 완벽하게 독립된 병렬 구역으로 인식하고, 제어하기 시작합니다.
여기에 앞에서 배운 부분 사전 랜더링(PPR)과 스트리밍이 엔진으로 결합되어
화면의 골격과 정적인 부분을 0.001초만에 보여주고
지연되는 데이터는 스피너를 돌려두어 백그라운드에서 완성되는 즉시 화면에 꽂아넣습니다.
직접 실습을 통해 알아보겠습니다. 다음과 같은 폴더 구조로 파일을 만들 것입니다.

여기서 병렬 라우트의 핵심은 폴더 이름 앞에 붙이는 @ 기호에 있습니다.
Next.js 엔진은 폴더 이름 앞에 @가 붙어있는 것을 확인하는 순간 일반적인 URL 주소로 취급하지 않습니다.
대신 이것을 화면의 특정 위치에 마음대로 끼워넣을 수 있는 독립적인 부품 즉, Slot으로 인식합니다.
병렬 라우트는 하나의 주소 안에서 여러 개의 독립적인 컴포넌트 생명주기를 동시에 가동하는 랜더링 방식입니다.
이제 독립된 슬롯들을 하나의 화면으로 조립해 낼 공장이 필요합니다. 그 역할은 layout.tsx 파일이 담당합니다.

먼저 layout.tsx 코드를 작성해보겠습니다.
import React from 'react'
export default function DashboardLayout({
children,
analytics,
team
}: {
children: React.ReactNode, // page.tsx 내용
analytics: React.ReactNode, // @analytics 폴더 내용
team: React.ReactNode // @team 폴더 내용
}) {
return (
<div className="p-8 bg-gray-50 min-h-screen">
<h1 className="text-3xl font-extrabold mb-8 text-gray-900">
회고 관리자 대시보드
</h1>
<div className="grid grid-cols-12 gap-6">
<div className="col-span-1 lg:col-span-8 bg-white p-6 border border-gray-100">
{children}
</div>
<div className="col-span-12 lg:col-span-4 flex flex-col gap-6">
<div className="bg-white p-6 border border-gray-100">
{analytics}
</div>
<div className="bg-white p-6 rounded-xl border border-gray-100">
{team}
</div>
</div>
</div>
</div>
)
}컴포넌트의 매개변수인 props를 유심히 보면, 기본적으로 제공되는 children 외에
우리가 @를 붙여 만들었던 폴더의 이름에서 @만 뺀 이름이 변수로 주입되고 있습니다.
우리는 화면의 원하는 위치에 { }를 열고 내려놓기만 하면 됩니다.
이제 analytics 슬롯에서 무거운 데이터베이스 조회가 10s 동안 있는다고 가정해도
다른 슬롯은 기다리지 않습니다.
더 나아가, @folder 내부에 loading.tsx를 추가하면 개별 스켈레톤 UI가 적용됩니다.
하지만 완벽해 보이는 병렬 라우트 시스템에도 반드시 제어해야만 하는 엣지 케이스가 있습니다.
사용자가 웹 브라우저에서 새로 고침(F5) 버튼을 누르거나, URL를 공유했을 때 발생하는 하드 네비게이션 경우입니다.
사용자가 새로고침을 누르는 순간, 브라우저는 기억을 잃고 서버에 완전히 새로운 화면을 요구하게 됩니다.
예를 들어, 대시보드만 보다가 @team 슬롯 안에서만 특정 상세 페이지로 이동했다고 가정합시다.

이 때 새로고침을 누르면, 프레임워크는 현재 URL인 dashboard 하위에 team-detail 주소에 맞춰
모든 슬롯을 다시 그리려고 시도합니다.
문제는 @analytics 슬롯에는 team-detail 페이지가 존재하지 않는다는 것입니다.
프레임워크는 이 주소에 그려야 할 analytics 부품을 찾을 수 없다며 404 Not Found 에러를 내뱉습니다.
유연성을 위해 화면을 쪼갰는데 오히려 그것 때문에 화면이 깨지는 역설적인 상황에 직면합니다.
바로 이 딜레마를 위해 등장하는 핵심 파일이 default.tsx입니다.
원리는 각 @slot folder 내부에 page.tsx와 default.tsx라는 파일을 만들어 두기만 하면 됩니다.
새로 고침을 눌러 기억이 날아갔거나 매칭되는 주소를 찾지 못했을 때 미리 준비해둔 default.tsx가 랜더링됩니다.
따라서 병렬 라우트를 설계할 때는 각 슬롯마다 default.tsx를 설계하는 것이 시스템 안정성을 위한 필수 의무입니다.
상태와 URL의 동기화
화면 위에 떠오르는 모달이나 팝업을 단순히 React의 상태, 즉 휘발성 메모리인 State로만 관리하면 안됩니다.

예시로, 아래와 같은 온라인 학습 대시보드를 개발한다고 상상해봅시다.

화면 중앙의 K강(K는 어떤 정수) 핵심 요약본 버튼을 클릭합니다.
그 순간 프론트엔드 내부의 상태값, 즉 isOpen 변수가 true로 변경됩니다.
화면 결과는 대시보드 화면 위에 핵심 요약본 모달이 아름답게 떠오릅니다.

하지만 주의깊게 봐야할 것은 주소창입니다. 화면은 바뀌었지만, URL은 여전히 /dashboard에 머물러 있습니다.

이 주소의 URL을 복사해서 공유하려고 하면, 상대방의 브라우저 화면은 사라진 모달의 화면일 것입니다.
State가 메모리 상의 변수라서 상대방의 브라우저에서는 false 값으로 초기화된 것입니다.

바로 이것은 개발자가 웹의 URL을 무시하고, 임시 기억장치인 State에만 의존하여, 화면을 그렸기 때문입니다.
URL은 인터넷이라는 거대한 공간에서 특정 정보의 위치를 영구적으로 증명하는 절대적인 주소입니다.
즉 사용자가 보고 있는 현재의 화면 상태가 반드시 URL이라는 절대 주소와 완벽하게 일치 즉 동기화되어야 합니다.
그렇다면, 주소 자체를 명확하게 이동시키면 어떻게 될까요?
이 때는 URL이 완전히 바뀌었으므로, 브라우저는 화면 전체를 새로고침하는 하드 네비게이션을 발생시킵니다.
그 결과 모달 뒤에 깔려있었던 기존의 문맥(컨텍스트)이 완전히 날라가 버리고, 새하얀 페이지에 모달만 덩그러니 남아있습니다.
우리가 궁극적으로 원하는 것은 배경의 문맥(컨텍스트)를 굳건히 유지한 채 그 위에 모달을 띄우는 것입니다.

해결법으로, 병렬 라우트로 배경을 단단히 고정하고, 그 위에 새로운 라우팅 마법을 결합하는 것입니다.
사용자가 클릭해 이동하려는 라우트의 흐름을 낚아채는 Next.js 16이 제공하는 **인터셉팅 라우트(Intercepting Routes)**입니다.
즉 기존의 맥락을 유지하면서도 URL만 절대 주소로 바뀌는 초월적 구조가 가능해야 합니다.

이 마법같은 가로채기를 코드로 지시하기 위해 괄호 안에 .을 찍어 우리가 가로챌 라우트의 경로 수준을 지정합니다.
(.) 하나는 같은 폴더 수준을 가로채겠다는 뜻이고, (..) 두개는 한 단계 상위 폴더 수준을, (..)(..)는 두 단계 상위 수준을
(...)는 아예 프로젝트의 최상단 루트 즉 앱 수준을 가로채겠다는 아키텍트의 명시적 선언입니다.

이해를 돕기 위해 사진 피드 애플리케이션의 폴더 구조를 알아보겠습니다.

/photo/[id] 라우트의 진짜 Photo 페이지가 존재함에도 불구하고
우리는 feed 폴더 내부에 (..)photo라는 특이한 이름의 폴더를 심어두었습니다. 이것이 아키텍트의 함정입니다.
이제 피드 라우트에서 사진 보기를 클릭하면 원래 photo 폴더로 페이지를 보내지 않고
대신 한 단계 위인 photo 폴더를 향한 링크 요청을 가로채어
함정으로 파둔 (..)photo 컴포넌트를 피드 레이아웃 안에 랜더링합니다.
하지만 여기서 새로고침(F5)을 누르거나, URL을 복사하여 공유한다면 함정을 무시하고
독립된 페이지인 /photo/[id] 페이지를 랜더링합니다.
이것이 상태(State)와 URL의 동기화 딜레마를 타파하는 문제 해결 아키텍처입니다.
내부에서 클릭하여 돌아다닐 때는 기존의 맥락을 완벽하게 유지하여 사용자에게 빠르고 매끄러운 경험을 제공하고
외부에서 URL을 검색하고 들어왔을 때는 절대적인 주소의 규칙을 따라 독립된 전체 화면을 랜더링합니다.

여기서 우리의 궁극적인 목적은 화면의 일부를 교체하는 것에 멈추지 않고
기존 피드 화면이 뒤에 깔려있는 상태에서 반투명 배경 위에 사진이 우아하게 떠오르는 팝업 모달을 구현해야 합니다.
이 이면에는 프론트엔드 엔지니어링의 컴퓨터 과학적 딜레마가 숨어있습니다.
그것은 브라우저가 페이지 이동을 처리하는 소프트 네비게이션과 하드 네비게이션의 충돌입니다.
이제 시스템의 복잡성을 통제하려면 이 2가지 랜더링 분기를 완벽히 이해하고
극단적 예외 상황인 엣지 케이스를 극복해야 합니다.
먼저 소프트 네비게이션 상황에서는 기존의 피드 화면의 기억 즉 DOM과 State가 메모리에 온전히 보존되어
프레임워크가 피드 화면 위에 모달이라는 층(Layer)을 더 얹고, 그 안에 사진 데이터를 부드럽게 밀어넣습니다.
반면에 하드 네비게이션에서는 모든 기억을 잃어버리며, F5를 누르거나 현재 주소창을 입력했을 때가 그 경우입니다.
이전의 피드 화면에 대한 모든 기억을 상실하게 되고, 메모리는 텅 비어버렸고 처음부터 다시 그려달라고 거칠게 요구합니다.
이것이 모달 아키텍처의 가장 치명적인 엣지 케이스이며, 이 문제를 해결하기 위한 아키텍트의 해결책이 랜더링 분기입니다.
동일한 URL로 접근하더라도 사용자가 어떤 경로를 통해 들어왔는지에 따라 화면을 두 갈래로 나누어 분기하여 그려내야 합니다.
예를 들어, 소프트 네비게이션 경로라면, 흐름을 가로채어 기존 화면 위에 반투명한 모달 팝업을 띄워야 합니다.
하지만, 하드 네비게이션 경로라면, 가로채기를 과감히 포기하고, 꽉 찬 단독 원본 페이지를 그려내야 합니다.
Next.js는 이 복잡한 랜더링 분기를 병렬 라우트와 인터셉팅 라우트의 결합으로 해결합니다.
예를 들어 다음과 같은 구조를 살펴봅시다.

우리는 화면 위에 모달을 띄울 @modal이라는 병렬 슬롯을 만들고, 그 안에 (..)photo라는 가로채기 함정을 파둡니다.
이제 내부에서 사진을 클릭하면, 이 함정이 발동해 @modal 슬롯에 팝업이 뜨게 되고
배경인 feed 밑에 page.tsx는 문맥이 그대로 유지됩니다.
하지만 외부에서 URL을 치고 다이렉트로 접속하면 모든 함정을 무시하고 photo/[id]/page.tsx를 적용합니다.
레이아웃 붕괴도 404 에러도 없는 완벽한 방어선이 구축된 것입니다.
브라우저의 히스토리 스택
위에서 만든 팝업 모달의 경우에서 뒤로 가기 버튼을 누르는 순간
모달만 예쁘게 닫히는 것이 아니라 화면 전체가 과거로 날아가 버리거나, 아예 사이트를 이탈해 버립니다.
화면에 모달이 그저 시각적 레이어가 추가 된 것으로 착각하는데, 그게 아니라 URL 변경은 새로운 페이지 이동으로 간주합니다.
그래서 자신만의 깊은 기억 장치에 단단히 기록해둡니다.
이 기억장치가 바로 히스토리 스택(History Stack) 입니다.
메인 페이지에서 Feed 경로로 이동 후 한 번 푸시스테이트를 하고,
사진을 클릭해서 모달이 뜨면 새로운 경로로 이동하는 것으로 간주하고 스택에 푸시스테이트를 합니다.

이 상태에서 모달 우측 상단의 닫기 버튼을 눌렀다고 가정해봅시다.
만약 화면을 피드로 되돌리기 위해 router.push() 함수를 사용하여 피드 경로로 강제 이동하도록 지시했다면
스택에 있는 맨 위 접시를 치우는 것이 아니라 새로운 Feed 접시를 추가하게 됩니다.

결과적으로 스택은 Feed - Modal - Feed 라는 3개의 접시를 겹겹이 가지게 됩니다.
여기서 실제 뒤로 가기 버튼을 누르면, Feed가 벗겨지고 다시 Modal이 튀어나옵니다.
여기서 알아야 할 점은 스택은 쌓는 것(push)가 아니라 치우는 것(back)입니다.
Next.js 16에서는 이것이 router.back() 이라는 직관적인 함수로 제공됩니다.
또한 때로는 URL만 살짝 바꾸고 스택에 새로운 이동 기록은 남기고 싶지 않으려면 어떻게 해야할까요?
이 때는 새로운 접시를 쌓는 대신 맨 위 접시를 다른 접시로 바꿔치기하는 router.replace() 함수를 사용합니다.
여기서 뒤로가기 시에는 기존의 변경 전 Feed는 건너 뛰게 됩니다.