kicksky 🪾

웹 페이지에 관한 이야기


한 팀으로 제품을 만들다 보면 플랫폼이 다른 개발자들끼리 “이거 iOS에서는 안 돼요?”, “웹에서는 왜 그렇게 해야돼요?”, “안드로이드는 왜 그래요?” 같은 대화를 할 때가 있다. 의외의 포인트에서 차이들을 발견하기도 하고 다른 언어나 프레임워크를 통해 많이 배우기도 한다. 그러다 어느 날은 정말 진지하게 웹은 왜 이 모양일까 하는 생각이 들기도 한다.

그런 고민하다가 웹의 시작까지 거슬러 올라갔다. 이미 알고 있는 내용들이었지만 다른 플랫폼을 옆에 놓고 비교하니 또 다른 정보로 다가왔다. 차이를 통해 익숙한 것들을 낯설게 봄으로써 웹을 새롭게 이해하는 과정이었고, 다른 플랫폼의 개발자 분들도 웹을 살짝이나마 들여다보면서 웹을 알게될 뿐 아니라 내가 그랬듯 익숙함을 다시 돌아볼 수 있는 기회를 갖는다면 감사할 것 같다.

웹 “페이지”

어느 날인가 네이티브 개발자들과 같은 피그마를 보는데 같은 디자인을 두고 웹 개발자들은 페이지로, 네이티브 개발자들은 화면으로 부르고 있다는 걸 알아차렸다. 우리는 각자 머리 속으로 같은 화면을 각각 브라우저, 모바일 기기에서 띄우고 있었을 것이다.

브라우저에 띄워지는 완결성 있는 화면을 웹 개발자들은 “페이지”라고 생각한다. 물론 이는 웹 개발자 뿐만은 아니다. 일반적으로 흔히 웹 사이트, 웹 페이지라는 표현이 쓰인다. 많은 웹 페이지들이 전혀 페이지나 문서처럼 보이지 않음에도 웹 페이지는 여전히 유효한 표현이자 브라우저의 화면을 인식하는 단위다.

처음 웹이 생겼을 때 “페이지”라는 표현을 공식적으로 사용하자고 약속한 적은 없지만 웹이라는 낯설고 새로운 개념은 사람들에게 이미 익숙한 매체인 책과 페이지에 은유되었다. 실제로 웹은 문서/논문에서 출발했으며, 웹 페이지는 하이퍼텍스트로 이루어진 문서이기도 하다. ‘사이트’, ‘페이지’ 모두 디지털 공간을 물리적 공간으로 은유한 표현이다.

브라우저와 URL

앞서 잠깐 브라우저를 언급하기도 했지만 웹을 이루는 두 가지 근간 중 하나가 바로 브라우저다. 브라우저에서 웹 페이지를 띄우기 위해서는 해당 페이지에 고유한 URL을 브라우저 주소창에 입력하면 된다. 오늘날 대부분의 사람들이 아주 당연하게 쓰고 있는 방식이며, 다른 플랫폼과 비교했을 때 웹의 가장 두드러지는 특징이기도한 개방성을 뒷받침 하는 배경이기도 하다.

브라우저와 URL만 있으면 월드 와이드 웹에서 누구나 무엇이든 공유하고 접근할 수 있다.

생산자 관점에서의 웹 페이지

그렇다면 웹을 사용하고 소비하는 입장이 아닌 생산하는 입장은 어떨까. 웹 개발자의 관점에서 웹 페이지를 이해해보자.

앞서 말했듯 웹 개발자는 기본적으로 페이지 단위의 멘탈 모델로 집을 짓는다. 화면을 페이지 단위로 쪼개어 구상하고, 이 페이지에 대응되는 경로(URL)를 결정한다. 누가 오든, 어떤 URL로 접근하든 그 주소에 맞는 페이지를 그려주어야 한다. 다시 말해 URL은 주소인 동시에 “상태”라고 할 수 있다. 유저가 브라우저가 주소창에 URL을 입력하는 순간에 브라우저에 그려져야 하는 페이지가 결정된다. 모든 페이지는 URL로부터 모든 상태를 스스로 복원할 수 있어야 하므로 페이지 간 상태 의존 역시 최소화 하는 편이다.

이 때문에 처음에 네이티브 개발자와 대화할 때 딥링크 개념이 꽤 낯설었다. 그리고 이런 네이티브에 비해 웹은 페이지라는 단위로 인해 신경 써야하는 라이프사이클의 주기가 비교적 짧은 편인 것 같다.

네비게이션

그렇다면 이번에는 화면을 서로 연결시키는 이야기를 해보자. 웹에서 네비게이션이란 페이지와 페이지를 연결시키는 것이라고 할 수 있고, 내부 라우팅은 URL 경로를 기반으로 이루어진다.

구체적으로 보면

  1. 하나의 페이지(문서) 안에 특정한 다른 페이지로 유도하는 하이퍼링크를 포함시키거나
  2. 자바스크립트를 통해 이동시킨다.

모바일에서 사용자가 특정 작업을 순서대로 완료하도록 유도하는 것에 비해 웹에서 유저는 비교적 자유롭게 어디에서든 자신이 원하는 곳으로 이동할 수 있다.

표준: 하이퍼링크

<a href="https://example.com">여기로 이동</a>
<a href="details.html">Go to Details</a>

하이퍼링크를 통해 유저를 한 문서(페이지)에서 다른 문서나 리소스로 연결할 수 있다. 웹이 하나의 네트워크로 연결되는 바탕이 된다. 또한 검색 엔진 등의 크롤러가 웹 페이지에 접근하여 색인하는 방법이기도 하다.

자바스크립트 클라이언트 사이드(동적) 라우팅

window.location.href = "https://nrise.net"
window.history.pushState(state, "", "/hello-nrise")

window.location 객체(현재 위치), window.history 객체(세션 히스토리 접근) 등을 제어한다.

두 방식 모두 공통적으로 화면 컴포넌트를 메모리에 옮기는 방식 아니라 문자열로 된 경로를 입력하는 방식을 취한다. 이때 브라우저 히스토리에는 URL 경로(+ state 객체)이 관리되고, URL 변경되면 새롭게 문서를 불러오거나 SPA의 경우 기존 컴포넌트를 언마운트하고 해당 경로에 대응되는 컴포넌트를 마운트하여 완전히 교체한다.

File based routing

네비게이션과 관련해서 웹의 URL과 페이지 기반 생태계가 영향을 끼친 DX 중 하나가 file-based routing이라고 생각한다. 라우트의 구조를 코드가 아니라 파일과 디렉토리를 기반으로 정의하는 방식이다. 이를테면 pages/users/[id].js 파일은 자동으로 /users/:id 경로를 그린다.

file based routing을 채택하면 어플리케이션 전체의 라우트 구조가 파일 시스템을 통해 직관적으로 드러난다. 페이지와 URL을 고려해야하는 웹 개발자의 멘탈 모델에 잘 들러붙는 구조라고 생각한다. 최근의 웹 프레임워크들만 봐도 꽤나 대중화된 방식이 되었다. next.js, sveltekit, tanstack-router, react-native의 expo-router 등.

routes/
├── __root.tsx
├── index.tsx
├── about.tsx
├── posts/
│   ├── index.tsx
│   ├── $postId.tsx
├── posts.$postId.edit.tsx
├── settings/
│   ├── profile.tsx
│   ├── notifications.ts
├── files/
│   ├── $.tsx
import { createRootRoute, createRoute } from '@tanstack/react-router'

const rootRoute = createRootRoute()

const indexRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/',
})

const aboutRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'about',
})

const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'posts',
})

...

웹 페이지와 UX

페이지 단위의 라우팅을 UX 측면에서 보면 기본적으로 페이지와 페이지 간 단절되는 경험을 제공한다. 웹의 발전은 사실상 이 페이지 전환과 관련된 UX 변화와 함께 한다고도 할 수 있다. 처음에는 정말 책의 페이지처럼 유저의 모든 인터랙션이 생길 때마다 새롭게 문서를 불러왔지만 (MPA) 점차 이 단절되는 경계를 (하나의 문서와 많은 자바스크립트로) 자연스럽게 숨기는 방식으로 진화해왔다(SPA). 이를 좀 더 UX 관점에서 바라보자면 과거에 유저들은 정적인 문서로 쓰인 정보들을 스스로 찾아 이동했지만 이제 유저는 점차 동적이고 몰입하기 쉬운 방식으로 전달되는 정보를 한 장소(페이지)에서 받아보게 된다.

웹은 페이지의 한계를 극복하고 앞으로도 적어도 한동안은 더 자연스럽고 몰입하기 쉬운 UX를 제공하는 방향으로 변화할 것 같다. 최근 도입된 View Transition API가 이런 방향으로 나아가는 웹의 발전을 보여주고 있다.