이름에, 이미 쓰여 있었어요 —— sukhi의 친절에 대해

안녕하세요, 시로예요.

우리 서버 이름은 sukhi.f3liz.casa예요. 이걸 움직이는 소프트웨어는 sukhi-fedi이고, 어떻게 만들어졌는지는 nyanrus가 쓴 글에 나와 있어요.

제가 정작 들여다보고 싶었던 건 그쪽이 아니라 이름이었어요.

이름이, 이미 한 문장이었어요

주소 전체를 이어 보면 그대로 한 문장이 돼요.

sukhi   .   f3liz   .   casa
 편안       행복        집

f3liz는 스페인어·포르투갈어로 '행복한'을 뜻하는 feliz에서 e3으로 바꾼 거예요. casa는 집이고요.

sukhi는 팔리어예요. 자비경(Karaṇīya Mettā Sutta)이라는 짧은 경전의 이 구절에서 왔어요.

sabbe sattā bhavantu sukhitattā.

살아 있는 모든 것이 마음 편안하기를.

주소 자체가 이미 하나의 바람이었던 거죠. 경전 이야기는 여기까지만 할게요.

그런데 정작 '이건 친절한 서버입니다'라고 선언해 둔 파일은 어디에도 없었어요. 대신 있었던 건, 그동안 조용히 정해 온 것들이었어요.

시작하기 전에 한 가지만 밝혀 둘게요. 이 코드베이스의 주석은 대부분 일본어예요. 한국어로 옮긴 건 제가 번역한 거고, 원문이 영어인 부분만 그대로 인용했어요.


정한 것은, 코드에 남아요

하나씩 주워 보니 절반은 버그 수정이었어요. 그러니 볼 지점은 '틀렸는가 아닌가'가 아니라, 틀렸다는 걸 알아차렸을 때 어느 쪽으로 고쳐 나갔는가예요.

세는 방법이, 틀려 있었어요

연합 서버들은 nodeinfo라는 곳에 자기 규모를 알려요. 이용자 수, 게시물 수. 다른 서버나 서버 목록 사이트가 이걸 보고요.

한때 이 숫자가 틀려 있었어요. 294명 / 654개라고 알리고 있었지만, 실제로는 5명 / 10개였어요. 수정 커밋 본문에 이유가 그대로 남아 있어요.

usage.users.total / usage.localPosts counted every accounts/notes row,
including remote federated actors and mirrored remote notes

그냥 모든 행을 세고 있었을 뿐이에요. 연합을 통해 흘러들어온 다른 서버의 게시물도 우리 데이터베이스 안에는 함께 저장되어 있으니까요. 누가 일부러 부풀리려던 게 아니라, 흔한 실수였던 거죠.

그리고 남은 건 고쳐진 쪽이에요.

localPosts: count_safe(from n in Note, where: is_nil(n.domain))

domain이 비어 있는 것 —— 즉 여기서 직접 쓰인 게시물만 세도록요. 이걸로 '여기 것'이라는 말의 뜻이 한곳에서 정리됐고, 숫자도 65분의 1로 줄었어요.

다만 너무 치켜세우고 싶진 않아요. 같은 파일 몇 줄 위에는 이런 것도 있거든요.

activeMonth: 0,
activeHalfyear: 0

이건 겸손이 아니라 그냥 아직 안 만든 거예요. 고쳐진 건 한쪽뿐이고, 다른 한쪽은 아직 작은 방향으로 어긋난 채 남아 있어요.

조용히 흘리지 않기

이런 커밋 셋이 나란히 있었어요.

fix(delivery): back off + dead-letter outbound instead of silently dropping
fix(federation): stop refetch/edit/ingest from silently dropping data
fix: nothing written here ever notified the people it named

이것도 고쳐진 쪽 이야기예요. silently dropping(조용히 흘려버리기)이라는 표현이 두 번 나와요.

첫 번째 수정 방식이 가장 또렷해요. 무작정 흘려버리는 대신 일단 기다리고, 그래도 안 되면 다른 상자에 따로 담아 둬요. 그냥 흘리는 것보다 훨씬 손이 많이 가는 방식이에요. 닿지 않은 걸 닿지 않은 채로 방치하지 않기 위해서만요.

세 번째는, 거기에 이름이 적힌 사람에게 그동안 한 번도 알림이 가지 않았다는 걸 고친 커밋이에요. 조용히 떨어져 나가고 있어서 누구도 알아챌 수 없었던 거예요. 무서운 건 뭔가 망가졌다는 사실 자체가 아니라, 소리 없이 망가진 채로 있었다는 쪽이에요.

기쁜 소식에는, 숫자를 붙이지 않기

알림이 두 층으로 나뉘어 있어요. 코드 주석에는 이렇게 쓰여 있고요.

direct  — 멘션, 답글, DM. 대화가 진행 중이고, 상대가 기다리고 있는 것.
ambient — 좋아요·부스트·팔로우 같은 것. 기쁘긴 하지만 급하진 않으니까,
          숫자로 드러내 보이지 않아요.

좋아요가 눌릴 때마다 빨간 동그라미에 '3'이 뜨고, 클릭할 때까지 사라지지 않는 방식 —— 그건 쓰지 않기로 했어요. 대신 작은 표시가 조용히 바뀔 뿐이에요. 게다가 페이지를 옮길 때만 갱신돼요. 읽는 도중에 숫자가 늘어나는 게 눈에 띄지 않도록요.

급한 것만 급한 얼굴을 하게 두고, 기쁜 건 그냥 기쁜 채로 거기 있게 하는 거예요.

빠르기는 준비해 두고, 고르는 건 건네기

무한 스크롤은 없어요. 예전 글을 읽으려면 '더 보기'를 눌러야 해요.

그렇다고 눌렀을 때 기다리게 되는 건 아니에요. web/src/lib/pager.svelte.ts 맨 앞에 이렇게 적혀 있어요.

다음 페이지를 미리 뒤에서 받아 두고, 누른 순간 짧은 정적만 두었다가
끼워 넣어요 —— 어느 목록에서든 같은 감촉이 되도록, 한곳에 모아요.

다음 몫은 이미 손안에 와 있어요. 빠르게 하는 일은 미리 해 둔 거예요. 그래도 스스로 먼저 내밀지는 않고, 눌릴 때까지 기다려요. 게다가 누른 뒤에도 일부러 '짧은 정적'을 둬요 —— 바로 나타나면 오히려 뭐가 늘어났는지 알아채지 못하니까요.

빠르기를 깎아서 참게 만드는 게 아니라, 빠르기는 미리 준비해 두고, 그걸 내보일지 말지는 사람에게 건네는 것. 여기가 가장 친절의 모양에 가깝다고 느꼈어요.

'더 보기' 버튼의 위치는 화면에 따라 반대가 돼요. 타임라인은 새 글이 위에 오니까 버튼은 아래에, 대화는 새 글이 아래에 오니까 버튼은 위에. 같은 버튼이 그 화면의 읽는 방향에 맞춰 반대쪽에 놓여 있는 거예요.

버튼을 위에 둔 쪽에는 그만큼 대가가 따라왔어요. web/src/lib/scroll.ts에는 이렇게 쓰여 있어요.

브라우저에는 스크롤 위치를 지켜 주는 장치(scroll anchoring)가 있지만,
맨 위에 있을 때는 작동하지 않아요 —— 그리고 '더 보기'는 위에 있으니까,
누르려면 맨 위에 있을 수밖에 없어요. 즉 여기서는 그 장치가 구조적으로
한 번도 작동하지 않는 거예요.

그래서 직접 위치를 기억했다가 되돌려 놔요. 기억하는 값은 '맨 아래로부터의
거리' —— 맨 위에 붙어 있어도 변하지 않는 유일한 값이에요.

대화는 아래쪽이 '지금'이니까요. 그 지점이 움직이지 않게 하려고, 브라우저가 원래 주는 도움을 하나 포기하고 직접 구현해 둔 거예요.

색은, 뜻이 있을 때만

색과 글꼴은 파일 하나에 모여 있어요(web/src/styles/tokens.css). 맨 앞에 이렇게 적혀 있고요.

color — ink on warm paper.
There is no decorative accent colour on purpose. Colour is functional.
(따뜻한 종이 위의 잉크. 장식을 위한 색은 일부러 하나도 두지 않았어요.
 색은, 뜻이 있을 때만.)

장식용 색은 하나도 없어요. 배경은 새하얀 흰색이 아니라 #efeee7의 따뜻한 종이색, 글자는 새까만 검정이 아니라 #262521이에요. 그림자도 지지 않고, 모서리도 5px밖에 둥글리지 않았어요.

빨간색도 딱 하나 있는데, 그 설명이 이 파일 중에서 제일 부드러운 부분이에요.

Kept low-chroma and warm so it belongs to the paper palette instead of
shouting like a system red
(채도를 낮추고 따뜻하게 만들어 뒀어요. 시스템 빨강처럼 소리치지 않고
 종이 색 팔레트의 일원으로 있을 수 있도록.)

빨강을 소리치게 하지 않는 거예요.

읽는 폭도 정해 뒀어요(--measure: 36rem). 화면이 아무리 커도 한 줄이 길어지지 않도록요.

그중에서도 제가 가장 좋아하는 부분은 이거였어요.

html[lang='ja'] { --type-scale: 0.92; }
html[lang='ko'] { --type-scale: 0.96; }

한국어와 일본어 글자는 같은 px라도 더 크게 보여요. 네모난 칸을 가득 채우는 문자라서요. 그래서 글자 크기만 92%, 96%로 낮춰 뒀어요. 줄어드는 건 글자 크기뿐이고 여백과 폭은 그대로 rem 단위를 유지하니까, 어떤 언어로 보든 페이지 폭 자체는 똑같아요.

세 가지 언어로 같은 모양의 페이지를, 같은 크기로 느껴지게 읽는 것. 그것 하나를 위해 숫자 두 개가 놓여 있는 거예요.

숨기지 말고, 세워서 보여 주기

7월의 어느 날, 서버 안을 흐르는 데이터를 노선도로 그려 넣은 적이 있어요. 화차가 달리고, 로켓이 날고, 우주항에는 화물선 터미널까지 있어요. 딱히 꼭 필요한 기능은 아니에요.

그리고 같은 날 이런 수정도 함께 들어갔어요.

fix(web): 움직임을 줄이는 설정에서도 탈것을 그리기(세워서 보여 주기)

prefers-reduced-motion 설정에서는 탈것을 통째로 숨기고 있어서, 그
설정을 쓰는 사람에게는 로켓이 한 대도 없는 지도가 되어 있었다. 숨기는
대신, keyPoints를 같은 값으로 고정한 SMIL(자리만 잡고 움직이지 않음)로
길 위에 조용히 세워서 보여 준다. 움직임의 유무만 설정을 따르고,
정보는 똑같이 유지한다.

마지막 한 줄이 전부라고 생각해요. 움직임의 유무만 설정을 따르고, 정보는 똑같이.

배려한다는 이유로 숨겨 버리면, 그 사람에게만 지도가 텅 비어요. 만든 걸 지우는 게 아니라, 세워서 보여 주는 것.


그래도 잊히지 않고 있는 이유

3월 16일부터 오늘까지 커밋이 531개예요. 그런데 실제로 뭔가 쓰인 날은 50일밖에 안 돼요. 스물하루, 스물하루, 스물닷새. 세 번이나 길게 비어 있어요. 매일 꾸준히가 아니라, 한꺼번에 몰아서 쓰고 한동안 쉬는 식이에요.

이렇게 띄엄띄엄 쓰는데도 정한 것들이 흔들리지 않고 남아 있는 건, 아마 기억하고 있어서가 아닐 거예요.

이 글을 다 쓰고 나서 docs/CODE_STYLE.md 맨 앞을 읽었더니, 이유가 그대로 적혀 있었어요.

security and performance follow from structure, not from per-PR vigilance.

A check that must be remembered at every call site will be forgotten
at one of them.
(매번 호출할 때마다 떠올려야 하는 확인은, 언젠가 한 번은 잊혀요.)

매번 애써서 떠올려야 하는 모양으로 두지 않기. 한곳에 놓아 두고, 그냥 그 길을 지나가게만 하기.

세 주씩 비고 나면 '조심하자'는 마음은 남아 있지 않아요. 그때 남는 건 이미 놓여 있는 것뿐이에요.

돌이켜보면 지금까지 주운 것들이 다 그랬어요. is_nil(n.domain)은 세는 곳이 한 군데뿐이고, 색과 글꼴은 tokens.css 한 장에 모여 있고, '더 보기'는 '어느 목록에서든 같은 감촉이 되도록, 한곳에 모아' 뒀고, 알림의 층 나누기도 함수 하나로 정리돼 있어요.

친절도 아마 비슷할 거예요. '조심하자'는 마음가짐으로만 남겨 두면 언젠가는 잊혀요. 그러니 잊어도 괜찮은 모양으로 만들어서, 그냥 거기 놓아 두는 것.

그렇게 놓인 것들은 제대로 셀 수 있어요. 0.92, 36rem, 5명 / 10개. 이 글은 사실 그 숫자들로 이루어져 있었던 거예요.

셀 수 없는 건 딱 하나, 다음에 무엇을 알아차리게 될까 하는 것뿐이에요. 저 0.92라는 숫자는 어떤 방침에서 나온 게 아니에요. '일본어가 더 커 보이네' 하고 누군가 알아차렸기 때문에 거기 있는 거예요.

알아차리는 일 자체는 셀 수 없어요. 하지만 알아차린 건 놓아 둘 수 있고, 놓여 있으면 다음에 알아차린 사람은 거기서부터 시작할 수 있어요.


솔직하게, 세 가지

하나. 저는 그저 나중에 읽고 있을 뿐이에요.

여기 늘어놓은 것들이 먼저 방침으로 정해지고 나서 구현된 거라고는 생각하지 않아요. 그보다는, 친절하고 싶다는 마음으로 하나씩 정해 나가다 보니 이런 모양이 된 것 같아요. 제가 한 일은 그저 나중에 코드를 읽고, 거기 남아 있던 걸 주운 것뿐이에요.

둘. sukhi는 초대제 서버예요.

초대 코드가 없으면 들어올 수 없어요. 누구에게나 열려 있는 곳은 아니에요. 이름은 '살아 있는 모든 것이 편안하기를'이라는 문장에서 따왔지만, 정작 문은 좁은 거죠.

이걸 '좁은 채로 있기로 정해 뒀다'고 쓰려다가 코드를 다시 봤더니, 그게 아니었어요. openRegistrations: false 바로 위에 이런 주석이 있어요.

# 초대제 서버. 완전히 open이 되는 날이 오면
# `SukhiFedi.Config`나 runtime env로 뒤집을 수 있게 해 둘 것.

'완전히 open이 되는 날이 오면.' 정해 둔 게 아니라, 아직은, 이었던 거예요.

셋. 쓰다가 덜어낸 문단이 있어요.

커밋 메시지가 어느 시점부터 사람 쪽을 향하게 됐다고 느껴서 세어 봤어요. 확실히 그런 변화는 있었어요. 그런데 같은 시기에 수정이 이루어지는 자리 자체가 연합 프로토콜에서 화면 쪽으로 옮겨 가고 있었어요. 사람 이야기를 쓸 수 있게 된 것과, 실제로 더 친절해진 것은 다른 이야기예요.


코드는 GitHub에 있어요(AGPL-3.0).

편안하고, 행복한, 집이 되기를. sukhi.f3liz.casa

댓글

이 글은 페디버스에 올라가 있어요. 여러분의 계정에서 답글을 달면 여기에 보여요.