logo

DowanKim

공통컴포넌트를 사용하면 렌더링도 이점이 있나?

2026년 7월 5일

PLick

웹 회원가입·로그인, 그리고 "공용 컴포넌트는 무엇을 재사용하나" (KAN-246)

데스크톱 웹에 회원가입(/signup, 피그마 W7 205-2)과 로그인(/login, W6 206-2)을 얹었다. 두 노드를 나란히 놓으니 카드 형태·간격·버튼·색이 완전히 같고 다른 건 넷뿐이었다 — 태그라인, 버튼 동사("회원가입"↔"로그인"), 약관 문구 유무(로그인엔 없다), 하단 전환 링크. 그래서 카드 전체를 apps/web/app/_components/AuthCard.tsx 한 컴포넌트로 빼고 이 넷만 props로 받게 했다. 두 page.tsx는 props만 넘기는 열 줄짜리 껍데기가 됐다.

"같은 컴포넌트니까, 경로가 바뀌어도 재사용되지 않나?"

로그인에서 회원가입으로 넘어가면 둘 다 AuthCard를 그린다. 그러면 React가 카드를 그대로 두고 바뀐 props만 갈아끼우는(리렌더) 거 아닐까? DOM을 물려받는 이득이 공용화에서 생기는 걸까?

답은 아니오. /login → /signup 이동은 AuthCard를 통째로 언마운트했다가 새로 마운트한다. 리렌더가 아니라 리마운트다.

이유는 React의 재조정(reconciliation, 이전 트리와 새 트리를 비교해 무엇을 유지·교체할지 정하는 과정)이 "트리에서 같은 위치 + 같은 컴포넌트 타입"일 때만 인스턴스와 DOM을 유지하기 때문이다. 부모의 타입이 바뀌면 그 아래는 전부 버리고 새로 만든다. 지금 트리는 이렇다.

RootLayout
 └─ (page 슬롯)
     ├─ /login  → <LoginPage>  → <AuthCard …>
     └─ /signup → <SignupPage> → <AuthCard …>

/login/signup은 서로 다른 라우트 세그먼트(각자 page.tsx)이고, page 슬롯에 앉는 게 LoginPageSignupPage냐로 타입 자체가 다르다. 둘 다 안에서 <AuthCard>를 그리지만, React 입장에선 부모가 통째로 다른 타입으로 교체된 거라 그 아래 AuthCard까지 헐고 새로 만든다. React는 타입 경계를 넘어 서브트리를 옮겨 재사용하지 않는다. "같은 컴포넌트를 쓰니 알아서 이어붙겠지"는 일어나지 않는다. 게다가 이 두 화면은 서버 컴포넌트라 보존할 클라이언트 상태도 없다 — Next는 이동할 때 새 라우트의 RSC 페이로드를 받아 page 세그먼트만 갈아끼우고, 그 안의 DOM은 새로 생긴다.

그럼 공용화의 이득은 정확히 뭔가

렌더 재사용이 아니다. 딱 두 가지다.

하나는 코드가 한 벌이라는 것. 카드 마크업·간격·토큰을 한 곳에서 고치면 두 화면에 동시에 반영된다. 로그인과 회원가입이 시각적으로 어긋날 일이 구조적으로 없다. 다른 하나는 번들에 정의가 1벌만 실린다는 것(두 화면에 복붙했으면 2벌). 즉 유지보수와 일관성, 그리고 약간의 번들 절약이 전부다. "경로가 바뀌어도 DOM을 물려받는" 성질은 여기서 나오지 않는다.

진짜로 경로를 넘어가도 재사용하고 싶다면

방법이 없는 건 아니다. 두 가지다.

첫째, 두 화면을 한 라우트로 합치는 것. /login·/signup을 별도 라우트로 두지 말고 한 페이지에서 상태나 쿼리로 전환하면, AuthCard가 같은 위치·같은 타입에 머무르므로 마운트를 유지한 채 props만 패치된다. 이때 비로소 DOM·입력 포커스·진행 중인 애니메이션이 보존된다. 탭 전환식으로 로그인/회원가입을 오가는 UI가 이렇게 만든 경우다.

둘째, Next의 layout.tsx에 두는 것. 공유 layout은 라우트가 바뀌어도 리렌더·리마운트되지 않고 살아남는다(반대로 template.tsx는 이동할 때마다 리마운트된다). 여러 화면에 걸쳐 진짜로 살아있어야 하는 UI — 지속되는 헤더나 전환 애니메이션 컨테이너 — 는 layout에 올린다.

그래서 우리는 라우트를 분리해 뒀다

둘 다 채택하지 않았다. 로그인과 회원가입은 각자 URL을 갖는 게 맞다 — 공유·북마크·뒤로가기· 접근성 모두 별도 경로일 때 자연스럽다. 그리고 이 카드는 정적이라 리마운트 비용이 사실상 0이다. 보존할 포커스도, 끊기면 안 되는 애니메이션도 없다. 즉 "렌더 보존"이 필요한 상황이 아니다. 필요도 없는 재사용을 위해 두 화면을 한 URL에 욱여넣거나 카드를 layout까지 끌어올리는 건 과하다.

그래서 라우트는 분리하고, AuthCard는 오직 유지보수·일관성 목적의 공용화로 뒀다. 같은 이유로 AuthCard@plick/ui가 아니라 웹 _components에 뒀다 — 이 카드는 데스크톱 중앙 카드 레이아웃(min-h-dvh 중앙 정렬 + max-w-auth)에 결합돼 있고, 모바일 인증은 AppShell 기반 전체 화면 스택이라 뼈대가 다르다. 레이아웃까지 묶인 조각은 웹 안에서만 도는 게 맞다(ADR 0011의 앱 결합 판단과 같은 결). 반면 순수 프레젠테이션 조각인 SocialLoginButton은 토큰 유틸만 쓰는 앱 중립 컴포넌트라 @plick/ui로 승격해 모바일과 공유했다(사본 금지, 모바일 import 교체 포함).

공용화가 주는 건 "같은 걸 두 번 안 만든다"는 것이지 "화면을 넘나들며 살려둔다"가 아니다. 후자가 필요하면 컴포넌트를 공유하는 것만으론 안 되고, 라우트를 합치거나 layout으로 올려 위치와 타입을 유지해야 한다. 이 구분이 이번 작업에서 정리하고 싶었던 핵심이다.

관련: ADR 0013 웹 프로필 수정(직전 세션) · ADR 0011 공용 경계 · KAN-175 모바일 회원가입 · ADR 0012 웹 마이페이지