코드보다 UX 디자이너 흉내를 내고 있었다
어느 순간 코드를 고민하지 않게 됐다
최근에 예전에 썼던 코드를 다시 볼 일이 있었다. 그 코드가 어떻게 돌아가는지는 알겠는데, 왜 이렇게 만들었는지는 잘 설명이 안 됐다. 그냥 됐으니까 됐다. 더 이상 설명할 근거가 없었다.
그게 이상하다는 느낌이 들었다. 예전에는 상태 하나를 어디에 두는지, 이 함수가 부수 효과를 가져도 되는지, 이 인터페이스가 호출하는 쪽에서 얼마나 쉽게 쓰일 수 있는지 같은 것들을 계속 물어봤던 것 같다. 언제부터 그 질문들이 사라진 걸까.
급한 일이 쌓이면서 생긴 습관
이유를 찾는 건 어렵지 않았다. 일이 빠르게 쌓였고, 기능 하나를 최대한 빨리 치워내는 것이 주된 목표가 됐다. 깊이 고민할 시간이 없었다기보다, 그런 고민 자체가 느린 사람이 하는 것처럼 여겨지기 시작했다. 일단 돌아가면 된다. 다음에 다시 보면 된다. 그런 말들이 설득력 있게 들렸다.
문제는 그 논리를 나에게 강요한 사람을 속으로 욕하면서, 정작 나도 똑같은 방향으로 흘러가고 있었다는 점이다. 상사가 급하게 돌리는 방식이 잘못됐다고 생각하면서도, 그 속도에 적응하고, 그 속도에 맞는 방식으로만 일할 수 있는 사람이 되어가고 있었다. 욕을 하면서 닮아가고 있었다는 게 지금 생각하면 꽤 씁쓸하다.
어줍잖은 UX 디자이너
그렇게 몇 년이 지나고 나서 나는 소프트웨어 엔지니어라기보다, 디자이너도 아닌 무언가가 되어 있었다. 버튼 색깔이 어떤 게 나은지, 이 플로우에서 유저가 헷갈리지 않을지, 이 텍스트가 읽기 편한지 같은 것들에 시간을 쓰고 있었다. 그게 나쁜 일은 아니다. 그런데 나는 UX 디자이너가 아니고, 그 판단을 제대로 할 수 있는 훈련을 받지도 않았다.
엔지니어링은 뒤로 밀리고, 디자인은 흉내만 내고. 뾰족하다는 겉모습은 있었을지 몰라도 실제로는 얕고 좁게 일하고 있었다. 성장이라는 말이 민망해질 만한 시간이었다.
오래 걸린다는 핑계가 없어졌다
그런데 요즘은 그 핑계를 대기가 조금 어려워졌다. AI가 구현 속도를 상당히 올려줬기 때문이다. 예전에 하루 걸리던 작업이 두 시간에 끝나는 경우가 생겼고, 그러면서 남는 시간이 생겼다. 예전이었다면 "이건 나중에 제대로 하자"고 했을 일들을 지금 할 수 있게 됐다.
그러면 그 시간에 뭘 해야 하는지가 진짜 질문이 됐다.
다시 쌓아야 할 것들
순수 함수를 만드는 일부터 다시 시작하고 싶다. 보일러플레이트라고 생각해서 건너뛰었던 것들이다. 상태나 사이드 이펙트 없이 입력과 출력만 있는 함수들. 이게 쌓이면 단위 테스트를 쓰기가 훨씬 쉬워진다. 테스트가 있으면 리팩토링할 때 덜 무섭다. 테스트 피라미드가 제대로 쌓인 코드베이스와 그렇지 않은 코드베이스의 차이는, 기능 하나 추가할 때 드는 불안감에서 직접 느껴진다.1
예전에는 이게 느리다고 생각했다. 지금 기능을 치워내기도 바쁜데 테스트까지 쓰고 있을 시간이 어디 있냐고. 그런데 실제로 테스트가 없는 코드가 훨씬 느리게 만든다. 뭔가 바꿀 때마다 전체를 수동으로 확인해야 하니까. 그 비용이 쌓이면, 기능 하나 바꾸는 게 괜히 무거운 일이 된다.
상태 설계를 다시 생각해보면
상태도 문제다. 지금까지 상태를 설계할 때 대부분 그냥 나열했다. 이 화면에 필요한 값들을 쭉 적고, 각각을 useState나 전역 스토어에 담는 방식. 이렇게 하면 작은 화면에서는 괜찮지만, 커머스처럼 플로우가 복잡한 도메인에서는 금방 무너진다.
주문 플로우를 예로 들면, 배송지를 입력하기 전에 결제가 진행되는 상태라든가, 재고가 없는 상품이 장바구니에 담긴 채 결제 직전까지 오는 상황 같은 것들이다. 이런 상태들이 존재하면 런타임에서 에러를 잡아야 한다. if 문으로 막고, 예외 처리를 넣고. 그런데 그 상태 자체가 애초에 만들어지지 않으면 그 코드가 필요 없다.
Richard Feldman이 Elm 컨퍼런스에서 이야기한 것처럼, 불가능한 상태를 표현할 수 없게 타입 시스템을 설계하면 런타임 에러 자체가 줄어든다.2 Alexis King의 "Parse, don't validate"도 같은 맥락이다. 경계에서 한 번 제대로 파싱해서 올바른 타입으로 만들면, 그 이후에는 그 값이 유효하다는 걸 다시 확인할 필요가 없다.3 컴파일 타임에서 잡을 수 있는 걸 런타임까지 미루는 건, 유저에게도 나에게도 비효율이다.
상태 머신이 그래서 생겼다. 어떤 상태에서 어떤 상태로 전이가 가능한지를 명시적으로 정의하면, 불가능한 전이는 코드에서 아예 표현이 안 된다. XState 같은 라이브러리가 이걸 도와준다.4 플로우가 복잡할수록 이 방식이 유리하다. 상태가 어디에 있는지를 추적하는 게 아니라, 지금 어떤 상태에 있는지가 코드 자체에 드러나기 때문이다.
뭐부터 해야 할까
솔직히 말하면 아직 정리가 다 된 건 아니다. 어디서부터 시작해야 할지 감이 잡힌 것도 있고, 아직 흐릿한 것도 있다. 지금 당장 바꿀 수 있는 건 작은 것들부터다. 함수 하나를 만들 때 부수 효과를 최소화하려고 의식적으로 시도하고, 그 함수에 테스트를 하나씩 붙여나가는 일.
상태 설계는 좀 더 생각해봐야 한다. 현재 코드베이스에 어디까지 적용할 수 있는지, 처음부터 다시 설계하는 게 현실적인지. 그걸 정리하는 시간이 필요하다.
소프트웨어 엔지니어로 시작했는데 그 방향에서 멀어지고 있었다는 걸 인식한 것만으로도 지금은 충분한 것 같다. 다음 글을 쓸 때는 조금 더 구체적인 이야기를 할 수 있기를 바란다.
Footnotes
-
Martin Fowler, Ham Vocke, "The Practical Test Pyramid", 2018. 단위 테스트, 통합 테스트, E2E 테스트의 비율과 역할을 설명한다. 단위 테스트가 많을수록 피드백 루프가 빠르고 유지보수 비용이 낮아진다. ↩
-
Richard Feldman, "Making Impossible States Impossible", Elm conf 2016. Elm의 타입 시스템을 활용해 런타임에서 발생할 수 없는 상태를 컴파일 타임에 제거하는 설계 방식을 설명한다. ↩
-
Alexis King, "Parse, don't validate", 2019. 입력값을 검증하는 대신 경계에서 파싱해 올바른 타입으로 변환하면, 이후 코드에서 유효성을 반복 확인할 필요가 없다는 원칙을 다룬다. ↩
-
XState 공식 문서. 상태 머신과 상태 차트를 JavaScript/TypeScript로 구현할 수 있는 라이브러리. 어떤 전이가 가능한지를 명시적으로 정의해 불가능한 상태를 코드 수준에서 차단한다. ↩