thenullpage.com
주소가 나뉘는 자리
URL은 한 덩어리 문자열처럼 보이지만 정해진 조각으로 나뉜다. 조각마다 맡은 일이 다르고, 어느 값을 어느 조각에 두느냐가 설계의 대부분을 차지한다.
https://example.com:443/articles/42?sort=new#comments
https 스킴, 통신 방식
example.com 호스트, 어느 서버인지
443 포트, 생략하면 스킴의 기본값
/articles/42 경로, 무엇을 가리키는지
sort=new 쿼리스트링, 어떻게 추릴지
comments 프래그먼트, 문서 안 위치
포트는 스킴마다 기본값이 정해져 있어서 https는 443, http는 80이면 적지 않는다. 그 외의 번호를 쓸 때만 호스트 뒤에 콜론과 함께 붙인다.
여기서 프래그먼트는 성격이 다르다. 샵 기호 뒤의 값은 서버로 전송되지 않는다. 브라우저가 받아 문서 안에서 해당 위치로 스크롤할 때만 쓰고, 서버 접근 로그에도 남지 않는다. 그래서 프래그먼트에 담은 값으로 서버가 다른 응답을 내주게 만드는 설계는 처음부터 성립하지 않는다.
경로에 담기는 것
경로는 대상이 놓인 자리를 나타낸다. 여러 개를 묶은 모음에는 복수형 명사를 쓰고 그 아래에 개별 식별자를 붙인다.
/articles 글 모음
/articles/42 42번 글
/articles/42/comments 42번 글의 댓글 모음
모음이 하나뿐일 때도 복수형으로 통일하는 편이 낫다. /article/42와 /articles가 한 서비스 안에 섞이면 쓰는 쪽이 매번 어느 쪽인지 기억해야 한다. 규칙 자체가 무엇이냐보다 한 가지로 고정돼 있느냐가 중요하다.
중첩은 두 단계 정도에서 끊는 것이 무난하다. /users/3/articles/42/comments/7처럼 길게 내려가면 댓글 하나를 지우려고 상위 번호를 전부 알아야 하고, 소속 관계가 한 번 바뀌면 그 아래 주소가 전부 무효가 된다. 댓글이 스스로 고유 번호를 가진다면 /comments/7로도 닿을 수 있게 열어 두고, 중첩 형태는 소속이 조회 조건 역할을 하는 목록 자리에만 쓰는 편이 관리하기 쉽다.
경로와 쿼리스트링의 경계
둘을 가르는 기준은 간단하다. 무엇을 가리키는지는 경로에, 그 대상을 어떻게 추릴지는 쿼리스트링에 적는다.
GET /articles?status=published&sort=-created_at&page=2
이 조건들을 경로로 옮겨 /articles/published/recent/2처럼 만들면 조건이 하나 늘 때마다 새 경로 규칙이 필요하고 순서까지 외워야 한다. 반대 방향의 실수도 있다. 하나를 가리키는 주소를 /articles?id=42로 두면 겉보기에는 목록 주소인데 실제로는 낱개를 내주는 상태가 된다.
쿼리스트링은 물음표 뒤에서 시작하고 항목과 항목 사이는 앰퍼샌드로 잇는다. 값 안에 공백이나 앰퍼샌드처럼 구분자로 쓰이는 문자가 들어가면 퍼센트 형태로 바꿔 실어야 조건이 중간에서 잘리지 않는다.
값이 비어 있을 때의 처리도 미리 정해 둬야 한다. ?tag=를 태그가 비었다는 조건으로 볼지 조건이 없는 것으로 볼지 정해 두지 않으면, 화면마다 다른 결과가 나오는데 원인을 찾기 어렵다.
글자를 적는 방식
경로는 소문자로 통일한다. 호스트 이름은 대소문자를 구분하지 않지만 경로는 구분하기 때문에 /Articles와 /articles는 서로 다른 주소다. 게다가 서버가 파일 이름 대소문자를 구분하지 않는 환경에서 만들고 구분하는 환경에 올리면, 개발 중에는 열리던 화면이 배포 후 404로 바뀐다.
단어를 이어 붙일 때는 하이픈을 쓴다. 밑줄은 링크에 밑줄이 그어지면 가려 보이고, 낙타 표기는 대소문자 문제를 다시 불러온다.
/user-profiles 권장
/user_profiles 밑줄에 가려 보임
/userProfiles 대소문자 문제
끝의 슬래시도 정해 둬야 한다. /articles와 /articles/는 규격상 다른 주소라, 둘 다 같은 내용을 200으로 내주면 같은 문서가 두 주소로 존재하게 된다. 한쪽을 정본으로 정하고 다른 쪽은 301로 넘기는 것이 정리된 형태다. 경로 끝에 .json 같은 확장자를 붙이는 방식도 굳이 필요하지 않다. 어떤 형식으로 받을지는 Accept 헤더가 이미 맡고 있다.
식별자로 무엇을 쓸 것인가
순번 정수는 짧고 읽기 쉽지만 전체 규모가 그대로 드러나고, 번호를 하나씩 바꿔 가며 남의 자원을 열어 보려는 시도가 쉽다. 이건 주소 형식의 문제가 아니라 서버가 요청마다 소유자를 확인하지 않으면 뚫린다는 뜻이라, 번호를 감추는 것으로 해결할 문제는 아니다.
추측을 어렵게 하려면 무작위 식별자를 쓰지만 주소가 길어져 눈으로 읽기 어려워진다. 사람이 읽는 주소가 필요하면 제목을 옮긴 문자열을 쓰는데, 제목이 수정될 때마다 주소가 바뀐다는 약점이 있다. 번호와 문자열을 함께 두고 찾을 때는 번호만 쓰는 방식이 절충안으로 흔하다.
/articles/42
/articles/9f2c1a
/articles/42-url-design
한글을 주소에 넣으면 전송될 때 퍼센트 기호가 붙은 형태로 바뀐다. 주소창에서는 브라우저가 되돌려 보여 주지만, 복사해 메신저나 로그에 붙이는 순간 알아볼 수 없는 문자열이 된다.
한 번 공개한 주소
주소는 공개되는 순간 내 손을 떠난다. 북마크, 외부 사이트의 링크, 검색 결과, 대화방에 붙여 둔 링크로 흩어지고 그것들은 회수할 수 없다. 구조를 개편해야 한다면 옛 주소를 지우는 대신 301로 새 주소를 가리키게 둔다. 이때 옛 주소가 다시 다른 옛 주소를 거치도록 사슬을 만들면 그만큼 왕복이 늘어나므로, 한 번에 최종 주소로 보내는 편이 낫다. 자원 자체가 사라진 경우라면 억지로 다른 곳으로 넘기지 말고 404나 410으로 없다는 사실을 밝히는 것이 맞다.
주소에 넣으면 안 되는 값
인증 토큰, API 키, 비밀번호 재설정 코드 같은 값은 주소에 담지 않는다. 주소는 서버 접근 로그와 중간 프록시 기록, 브라우저 방문 기록에 그대로 남고, 그 페이지에서 다른 사이트로 이동하면 Referer 헤더에 실려 바깥으로 나간다. 이런 값은 Authorization 헤더나 요청 본문에 싣는다. 메일로 보내는 링크처럼 주소에 담을 수밖에 없는 경우에는 유효 시간을 짧게 두고 한 번 쓰면 무효가 되게 만든다.
확인하는 방법
중복 주소가 있는지는 같은 자원을 여러 형태로 불러 보면 드러난다.
curl -i https://example.com/articles
curl -i https://example.com/articles/
curl -i https://example.com/Articles
세 줄이 모두 200으로 같은 내용을 내주면 한 문서가 세 주소로 존재하는 상태다. 하나만 200이고 나머지가 301로 그쪽을 가리키면 정리된 것이다.
리다이렉트가 몇 번 일어나는지는 개발자 도구 Network 탭에서 확인한다. 옛 주소를 입력하고 목록 맨 위부터 훑었을 때 301이 연달아 두세 줄 쌓여 있으면 사슬이 생긴 것이다. 명령줄에서는 -L 옵션을 붙여 따라가게 한 뒤 중간에 찍히는 Location 헤더를 세어 봐도 된다.
쿼리 조건이 실제로 먹는지는 값을 바꿔 가며 응답을 비교하는 것이 가장 빠르다. sort 값을 바꿨는데 순서가 그대로이거나, 없는 조건 이름을 아무렇게나 넣었는데 오류 없이 전체 목록이 나온다면 서버가 그 값을 읽지 않고 있다는 신호다.