웹사이트나 API를 만들려면 URL 정의가 필요한데 데이터베이스 모델링이 만만치 않은 것처럼 URL을 정의하기도 쉽지 않고 API를 정의할 때랑 웹사이트의 URL을 정의할 때가 또 다르다. 웹사이트를 만들 때마다 URL을 어떻게 정의할지 고민하게 되는데 최근에 작업하려고 내가 좋아하는 사이트들이 URL을 어떻게 정의하고 있는지 살펴봤다.
stackoverflow
stackoverflow는 상당히 전통적인 개념의 REST에서 보통 말하는 교과서적인 URL을 그대로 사용하고 있었다.(URL 정의만 찾아본 것이므로 GET
/POST
외에 다른 HTTP 메서드도 사용하는지는 확인하지 않았다.) 여기서 교과서적인 RESTful URL이란 의미는 /teams/:teamName/players/:playerName
같은 식으로 리소스를 표현하기 위해서 컬렉션과 특정 리소스를 지정하는 방식을 의미한다.
- /users/ : 전체 사용자를 보여준다.
- /users/:userSeq/:userName : 특정 사용자의 정보를 보여준다. 그래서 내 URL은
/users/518864/outsider
가 된다. 여기서:userSeq
만 사용하지 않고:userName
까지 붙인 이유는 URL의 가독성 때문으로 보인다.:userSeq
같은 경우는 바뀌지 않지만:userName
은 바꿀 수 있으므로 저런 계층 구조로 만든 것이다. 그래서/users/:userSeq/
에 접속하면/users/:userSeq/:userName
로 리다이렉트가 되고/users/:userSeq/
뒤에 아무 문자나 붙여도 해당 사용자로 리다이렉트가 된다. - /questions : 전체 질문 목록을 보여준다.
- /questions/:questionId/:questionTitle : 특정 질문을 보여준다. 예시로
/questions/27815644/rotate-rectangular-element-90-degrees-on-a-grid
와 같은 URL이고 질문의 제목을:questionTitle
로 붙여주어서 URL을 읽을 수 있게 만들어 주었고 마찬가지로/questions/:questionId/
로 접근하면 전체 URL로 리ㄷ이렉트 해준다. - /tags : 태그 목록을 보여준다.
- /questions/tagged/:tagName : 특정 태그가 달린 질문 목록을 보여준다.
- /unanswered : 대답이 안 달린 질문을 보여주는 URL이다.
- /unanswered/tagged/:tagName : 대답이 안 달린 질문 중 특정 태그가 달린 질문을 보여준다.
- /users/signup : 회원 가입 페이지
- /users/login : 로그인 페이지
- /legal/privacy-policy : 이용약관 같은 경우는
/legal
등의 밑으로 붙지만 스택오버플로우 특성상 부모사이트인 http://stackexchange.com로 이동하므로 URL이 충돌하거나 하진 않는다.
- /:userId : 해당 사용자의 페이지를 보여준다. 루트 경로 아래에 바로 사용자 아이디를 사용하고 있는데 이는 사실상 와일드카드를 사용하고 있는 것과 마찬가지다. 이용약관, 회원 가입 등의 URL과 충돌하므로 내부적으로 라우팅의 우선순위를 주고 해당 URL의 예약어는
userId
로 가입을 못 하게 막는다던가 하는 처리를 한 것으로 보인다. - /:userId/status/:tweetSeq : 사용자의 특정 트윗을 보여준다.
- /:userId/following : 팔로잉 목록을 보여준다.
- /tos : 이용약관 페이지
- /signup : 회원가입
- /login/ : 로그인
Github
- /:userId : Github도 Twitter와 같게 루트 경로 아래 사용자 아이디를 사용해서 와일드카드를 쓰고 있다. 그룹의 경우
/:organizationId
로도 쓸 수 있지만 Github이 그룹과 사용자를 구분하지 않으므로 같은 패턴을 쓰고 있다./login
,/join
등이 있으므로 Twitter와 같게 라우팅 충돌을 막고 예약어의 회원 가입을 막는 등의 처리를 하고 있다. 실제로 회원 가입을 할 때 보면 사용 중인 아이디는Username is already taken
라고 나오지만login
등은Username is a reserved word
라고 표시된다. - /:userId/:repository : 저장소 페이지. 저장소가 사용자에 포함되어 있으므로 사용자 밑으로 저장소가 들어가고 저장소와 관련된 다른 메뉴들은 이 하위로 들어간다.
- /:userId/:repository/issues : 저장소의 이슈 목록
- /:userId/:repository/issues/:issueNo : 저장소의 특정 이슈
- /:userId/:repository/pulls : 저장소의 풀리퀘스트 목록
- /:userId/:repository/pull/:pullRequestNo : 저장소의 특정 풀리퀘스트(목록은
pulls
인데 특정 PR을 볼 때는pull
이네. 이슈에서는 둘 다issues
이면서..) - /login : 로그인
- /join : 회원가입 페이지
- /site/terms : 이용약관 페이지. 그외에도
/security
나/contact
등도 있지만 실제로는 https://help.github.com로 모두 리다이렉트 된다.(왜 바로 help.github.com으로 안 보내는지는 궁금하지만...) - /explore : 추천 프로젝트 보기
- /search : 검색 페이지
- /showcases : 쇼케이스 페이지
Medium
- /@:userId : 사용자 페이지. Medium은 특이하게 사용자 아이디 앞에
@
를 붙이는데 이는 루트 경로 밑에 와일드카드 사용으로 인한 예약어와의 충돌을 막기 위한 거라고 생각한다. 처음 URL에서@
를 보았을 때는 이상하게 느껴졌는데@
가 멘션을 의미한다는 일반적인 관례가 있으므로 예약어 충돌을 막을 수 있는 괜찮은 접근이라는 생각도 든다. - /@:userId/:postTitle-postId : 특정 글의 페이지. 약간 특이한 방식인데 예시로
/@ev/a-mile-wide-an-inch-deep-48f36e48d4cb
같은 URL이 된다. 제목 뒤에 글의 아이디로 보이는 값이 붙어있는데 추측 상으로는 같은 제목의 글을 작성했을 때 충돌을 막기 위함이라고 생각한다. 그래서 뒤에 글의 아이디가 붙어있더라도 제목부분에 다른 글자를 입력하면 404페이지가 나온다. - /me : 로그인했을 때의 사용자 페이지로 자신의
/@:userId
로 리다이렉트가 된다. 로그인하지 않았을 경우에는 로그인 페이지로 리다이렉트 된다. - /me/stats : 로그인한 사용자의 통계 페이지.
/@:userId
를 사용하지 않고/me
를 별도로 만든 이유는 잘 모르겠다. - /me/stories/drafts : 로그인한 사용자의 작성 중인 글 목록.
- /me/stories/public : 로그인한 사용자의 발행한 글 목록.
- /p/new-post : 새 글 작성 페이지.
- /p/import : 다른 사이트에서 글 가져오는 페이지. 글 작성과 관련된 부분은
/p
밑으로 붙는데 post를 의미하는 것 같다. 그럼에도 stories는/me
밑에 있어서 일관성은 떨어진다고 생각한다. - /p/:postId/edit : 글의 수정 페이지. 글의 URL은 앞에서 본대로
/@:userId/:postTitle-postId
이지만 여기서 수정을 누르면/p/:postId/edit
로 이동한다.(역시 일관성은 별로라는 생각이....) - /search : 검색 페이지
- /collections : 사용자가 구독 중인 컬렉션(글을 특정 그룹으로 모아놓은) 목록 페이지. 로그인 하지 않았다면 컬렉션 검색 페이지로 이동한다.
- /:collectionName: 사용자나 에디터가 만든 특정 컬렉션의 페이지로
/backchannel
같은 URL이 된다.(사용자의 경우@
로 와일드카드 충돌을 막았지만, 컬렉션 같은 경우는 루트 경로 밑에 와일드카드를 바로 사용했다. 이것 때문에@
를 사용했을 수도 있고...) - /m/signin : 로그인 및 회원 가입 페이지.
/m/
은 무슨 의미 인지 왜 붙는지 잘 모르겠다. - /policy/medium-privacy-policy-f03bf92035c9 : 이용약관 페이지. 사용자의 글과 거의 비슷한 형태이지만
policy
앞에@
가 붙지 않는다.
내 생각
상당히 대표적인 사이트임에도 URL 정리가 만만치 않구나 하는 생각이 든다. 개인적으로는 Github이 그래도 가장 깔끔한 URL이 아닌가 싶고 Medium은 너무 많이 고민하다가 오히려 꼬인듯한 생각이 든다. 나중에 기능이 추가되면서 레거시때문에 어쩔 수 없이 취한 선택도 있을 수 있고 서비스마다 특성이 다르기에 URL 정의에 정답은 없다고 생각한다.
다만 평소에 /*
식으로 첫 경로에 바로 와일드카드가 오는 것은 지옥의 문을 여는 것과 같다고 생각하고 있었는데 뜻밖에 꽤 많은 서비스가 이렇게 사용하고 있다는데 놀랐다.(심지어 깃헙은 저장소 주소는 /*/*
이다.) 내가 보통 하는 접근은 Stackoverflow 식의 <컬렉션>/<특정아이템>/<컬렉션>/<특정아이템>
같은 계층 구조이지만 URL이 상당히 장황해지는 단점이 있고 단축 URL 같은 서비스가 있지만(난 별로 안 좋아하지만) 서비스 자체가 간결한 URL 구조를 가진다면 좋을 것이다. 그냥 생각해도 /users/jquery/repository/jquery
보다는 /jquery/jquery
가 간결해지고 보기 좋다. 물론 전자는 의미가 명확하다는 장점이 있기는 하지만 서비스를 쓰다 보면 아이디란 걸 누구나 알 수 있으므로(github이나 twitter 등) 큰 차이 있나 싶기도 하다.
Stackoverflow나 Medium 같은 경우는 사용자나 글의 시퀀스번호를 URL에 넣어서 이용하고 있다. 과거에는 시퀀스번호로만 URL을 구성했는데(게시판의 경우 /board/2
처럼) URL 가독성이 중요해 지면서 타이틀이나 유저명을 혼합해서 표시하는 방식을 취하는 것으로 보인다. 물론 시퀀스번호를 아예 안 쓰는 곳도 많지만 시퀀스를 혼합해서 사용하는 방식은 약간의 절충형이라고 보인다. 어차피 의미 없는 시퀀스는 사람들이 무시하면 그만이니까..(괜찮은 방법 같기도 하고 아닌 것 같기도 하고...)1
-
물론 우리는 한글을 사용하므로 URL에 가독성을 주기 위해 한글이 붙이는 방식이 좋은가 아니냐도 또 하나의 관심거리이긴 하다. ↩
저 같은 경우 로그인한 사용자의 정보를 가져가는 REST API를 /users/:userId 대신 /users/me 라고 이름을 붙였습니다. 다른 사람의 정보를 반환하는 API는 필요가 없기도 했지만, 있다하더라도 아마 내 정보와 다른 사람 정보는 범위가 다를 텐데, /users/:userId에서 userId가 나인지를 검사해서 다른 정보를 반환하는게 불필요하다고 생각했습니다.
Medium에서 /me/stats 도 비슷한게 아닐까 싶습니다. /:userId/stats 였다면 괜히 다른 사람 통계를 보려고 시도하지 않을까요?
어느 쪽으로 하더라도 어차피 권한확인은 해야하므로 구현상에 큰 문제는 없다고 생각합니다. /:userId/stats 로 구현했다고 다른 사람의 정보를 모두에게 보여줄 수는 없으니까요. 평의상 축소할 수는 있다고는 생각하지만 Medium은 그런 부분이 좀 일관성이 없게 느껴져서요...
물론 저도 /me는 좋은 방식이라고 생각하고 (실제로 효과있을지는 모르지만) 공개용 URL과 구분할 수 있는 방법이라고도 봅니다. ㅎ
github에서 /:userId 가 나중에 생길 기능과 충돌이 나면 어떻게 할까요? /showcases 도 최근에 생긴걸로 알고 있는데 그 전에 /showcases 로 사용하던 사용자가 없었을까요? ^^
추가될 모든 기능을 다 예약어로 미리 잡아도 한계가 있을것 같아서요.
그런 충돌때문에 그동안은 / 밑으로 와일드카드가 오는 것을 별로 안좋아했는데요.... github 직원이 아니니 추측할 수밖에 없지만 미리 예상되는 걸 reserved로 등록해 놓거나 도메인 사듯이 없는데서 찾아내지 않을까요? ㅎㅎ
그러게요. github API가 깔끔하긴 한데 이런 부분은 차라리 미디엄처럼 /@:userId 가 더 나아보기도 하네요.
아무튼 좋은 블로그 글 고맙습니다~^^
저도 정답을 모르겠어서 설계전에 다른 사이트는 어떻게 했나 비교한걸 정리한건데 이쪽 저쪽 다 약간씩 장담점이 있어보이네요 ㅎㅎ
좋은 포스트 잘보고 갑니다 ^^