App Store에 noon을 올렸다. 살아 있는 눈 하나를 손목에 띄우겠다고 오래 만진 앱이다.
출시하고 몇 시간 뒤, 폰 화면에서 그걸 봤다. 워치가 아니라 폰이었다. 눈이 화난 표정을 짓는 순간, 씌워둔 픽셀렌즈가 뭉개졌다.
시그니처가 하필 거기서 깨졌다
noon에는 눈에 렌즈를 씌우는 커스텀이 있다. 그중 픽셀렌즈는 눈을 굵은 픽셀 격자로 덮는다. 매끈한 눈이 레트로 게임 화면처럼 각진 블록으로 바뀐다. 스토어 소개에도 앞세운 렌즈고, 나도 제일 아끼는 축이다.
그 렌즈가 angry에서 무너졌다. 눈이 평온할 땐 멀쩡한 격자였는데, 표정이 화난 쪽으로 넘어가면 픽셀 경계가 번지고 격자가 뭉개졌다.
폰에서 먼저 눈에 띄었다. 렌즈 프리뷰를 넘기다 angry에서 화면이 지저분해지는 걸 봤다. 이때만 해도 폰 프리뷰만의 문제인 줄 알았다.
편한 길이 하나 있었다
픽셀렌즈는 SKEffectNode로 만들어져 있었다. 이 노드는 자식들을 화면에 바로 그리지 않는다. 눈에 안 보이는 버퍼에 한 번 그린 다음, 거기에 필터를 먹여 내보낸다. 픽셀렌즈는 그 필터 자리에 픽셀화를 걸어, 눈이 무슨 표정을 짓든 그 위를 굵은 픽셀로 덮었다.
편한 길이었다. 눈 애니메이션은 손대지 않고, 위에 이펙트 노드 하나만 얹으면 알아서 픽셀이 됐다. 표정이 백 개든 렌즈 코드는 한 벌이면 됐다.
워치에선 그 길이 비쌌다
문제는 그 "한 번 더 그리는" 비용이었다. 워치에서 오프스크린 패스가 무거웠다. 성능이 눈에 띄게 처졌고, 하필 angry 표정에서 픽셀이 뭉개졌다.
왜 하필 angry에서 유독 심한지는 끝까지 깔끔하게 짚지 못했다. 다만 성능이 처지는 것도, 픽셀이 번지는 것도 화살표는 이 이펙트 노드 하나로 모였다.
폰만의 프리뷰 버그인 줄 알았던 게 오해였다. 같은 노드가 워치에도 그대로 올라가 있었고, 자원이 빠듯한 워치에서 오프스크린 패스는 더 안 맞았다.
필터를 걷고 블록을 직접 그렸다
이펙트 노드를 버리기로 했다.
픽셀렌즈가 결국 하려던 일은 단순하다. 눈을 굵은 픽셀 격자로 만드는 것. 그렇다면 그 격자를 필터에게 맡기지 않고 직접 그리면 된다. 몇 칸짜리 격자인지 정하고, 칸마다 사각형을 얹었다. 오프스크린 버퍼도 필터 패스도 없이 블록을 바로 화면에 그렸다.
바꾸고 나니 두 증상이 같이 사라졌다. 오프스크린 패스가 없어지니 성능이 돌아왔고, angry에서 번지던 뭉개짐도 없어졌다.

렌즈를 벗기며 자국을 안 지웠다
같은 렌즈를 만지다 하나 더 나왔다. 픽셀렌즈를 껐다 다시 켜면 격자가 어긋났다.
원인은 벗길 때 있었다. 렌즈를 해제하면서 이전 격자 상태를 리셋하지 않았다. 자국이 남은 자리에 새 격자를 얹으니 두 겹이 어긋나 깨져 보였다. 씌우는 코드만 들여다보다 벗기는 코드를 빠뜨린 것이다. 해제할 때 상태를 처음으로 되돌리게 하니 정리됐다.
출시하고 나서야 보였다
내 워치에서, 시뮬레이터에서 수도 없이 돌려봤는데도 이건 안 보였다. 정작 출시하고 나서, 워치보다 덜 들여다보던 폰 프리뷰에서 눈이 angry로 넘어가는 순간에야 드러났다. 픽셀렌즈 말고도 온보딩 권한 충돌, 염색 상태 꼬임 같은 걸 출시 직후 며칠 동안 열두 개쯤 고쳤다.
이번에 손에 남은 건 SKEffectNode 하나다. 오프스크린에 한 번 그리고 필터를 먹이는 이 노드는, SpriteKit에서 쓰던 대로면 그냥 얹기만 하면 되는 편한 도구였다. 워치에선 그 한 번 더 그리는 값이 성능과 화질을 같이 깎았다. 얹기 편하다는 이유로 골랐던 게 기기가 바뀌니 제일 비싼 부분이었다.
넓은 무대에선 가볍던 길이 좁은 곳에선 값이 붙었다.
눈이 처음으로 온갖 표정을 지어보는 걸, 출시하고 나서야 제대로 지켜봤다. angry에서 왜 유독 심했는지는 아직도 정확히는 모른다. 블록을 직접 그리게 바꾸고 나선 안 뭉개지니, 거기서 더 파지 않았을 뿐이다.