ECMAScript | TypeScript

배려하는 코드란 무엇일까

partner_jun 2025. 4. 16. 09:34

들어가며

최근 꽤 큰 프로젝트를 진행하고 있다. 3개 서비스의 회원을 통합해서 로그인하는 SSO 프로젝트다. 회원 통합이라는 이름으로 진행하는 이 프로젝트는 너무나 많은 기획 변경과 구조 변경으로 런칭도 전에 레거시화 되는 최악의 결과물을 만들어내고 말았다. 이 프로젝트를 진행하며 기술 내/외적으로 많은 것을 생각했고 얻었다. 그 중 하나를 적어두려 한다.

정말 끔찍하게 힘든 프로젝트였다

 

이전부터 계속해서 적어왔듯, 나는 프로젝트를 진행할 다음 개발자를 위해 코드를 작성해야 한다고 생각한다. 그런데 그게 어떤 코드인지에 대해 더 깊게 생각해볼 필요가 있다고 느꼈다. 다른 사람에게 이렇게 작성해 라는 가이드를 줄 수 없기 때문이다. 그래서 다음 FE 웹 개발자를 위해 어떻게 작성해야 할지 몇가지 고민했다. 거창한 내용은 아니지만 다음에 찾아볼 수 있게 적어둔다.

 

 

배려하는 코드란

맥락/호흡이 짧아야 한다.

첫번째로 호흡이 짧아야 한다. 가장 중요하게 생각하는 요소다. 유지보수 단계로 넘어갔을 때, 전체 맥락을 파악하라고 하는 것은 너무 무리한 요구다. 중간에 투입될 수 있는 다른 개발자를 위해서도 마찬가지다. 전체 코드를 작성한 개발자조차도 모든 것을 기억하지 못하는데, 그런 상황에서 이 부분을 고쳤을 때 어디에 어떤 영향이 갈지 몰라 라는 것은 정말 끔찍하다고 밖에 표현할 수가 없다. 심지어 최근 유행하는 LLM을 이용하려면 더더욱 호흡이 길어서는 안된다. 특정 코드 영역을 교체하는 것으로 단순하게 원하는 바를 이루어낼 수 있어야 한다. 한동안 유행했던 TDD 개발 역시 이 내용과 일맥상통한다. 적절한(이게 가장 어렵지만) 부분으로 코드 뭉치를 나눈다면 다른 부분에 영향이 없을 것이다. 그렇다면 FE 중심의 웹 개발에서는 어떻게 적용되어야 할까? 나는 두가지 부분으로 나누어 생각했다.

 

1. 페이지를 분리한다

로직을 처리하기 위해 302로 페이지를 이동시키는 방법을 고려한다. 굉장히 오래된(php 시절의) 처리 방식이기도 한데, Next.js와 같이 페이지 URL을 운용하는 경우가 많아 첫번째로 꼽았다. App Router든 Page Router이든 결국 클라이언트의 요청을 받아 처리하는 부분이 필요한데, 이 부분의 호흡을 짧게 해야 한다. 단순하게 함수로 분리하는 것도 있지만, 필요에 따라 페이지 자체를 나누는 것도 방법이다.

이번에 작업했던 프로젝트의 소셜 로그인이 그렇다. 소셜 로그인은 그 특성상 Redirect URL을 등록해놓고 로그인하면 소셜 코드를 가지고 URL로 이동하도록 되어있다(네이버/카카오/구글/애플 모두 동일한 정책이다). 그러다보니 하나의 페이지에서 여러 동작을 수행하고자 했고, 코드의 맥락을 잃고 각 파라미터의 의미나 로직 분기가 어려워졌다. 이 복잡한 로직을 코드로 풀어낼 수도 있겠지만 더 간단한 방법을 선택했다.

소셜 로그인 관련 플로우

 

역할의 분리를 위해 페이지 URL 자체를 분리한 것이다. 서버에서 페이지의 역할에 해당하는 로직만을 수행하고, state에 맞는 다음 스탭의 URL을 302로 응답한다. 이렇게 분리하면 사용자 경험 측면에 불편을 주지 않으면서 URL 별로 모듈화된 로직만을 구현할 수 있게 된다. 

 

 

2. 최대한 컴포넌트를 나누지 않아야 한다

재활용을 위해 굉장히 잘게 나누는 개발자들도 있지만, 너무 많은 컴포넌트를 만들면 코드를 파악하기 어려워진다. 또 사실상 코드를 재활용 하지 못한다. 앞서 말한 내용과 같이 전체 코드를 작성한 개발자조차 모든 것을 기억하지 못하는데, 중간에 투입된 개발자는 오죽할까. 같은 기능을 다시 개발할 확률이 높고, 이런 코드는 다시 한번 코드를 파악하기 어렵게 만든다. 너무 많이 나누어진 컴포넌트가 이번 프로젝트가 어려워진 이유 중 하나였다. 어떤 서비스로부터 진입했냐에 따라 다른 문구를 보여야 하는데, 이 문구가 길고 기능 차이(내가 보기엔 쿼리 파라미터의 차이 수준이지만)가 있어 다른 컴포넌트로 분리되었다. 그렇게 분리하게 되니 유사한 기능을 가진 컴포넌트가 굉장히 많고 재사용아닌 재사용을 하게 되었다

같은 컴포넌트지만 상태에 따라 보여질 컴포넌트가 다르다

 

문제는 QA 도중 발견한 문제를 수정할 때 생겼다. 그 유사한 컴포넌트가 어떤 상황에 노출되는지, 또 어떤 차이점을 가지고 있는지 파악하기 어려웠고, 심지어 진입한 페이지와 컴포넌트가 다대다 관계가 되어버림으로써 사이드 이펙트에 아주 취약한 상태가 되었다. 문구나 기능을 커스텀 훅을 이용해 페이지별로 모았다면 진입 경로에 따라 수정 여부를 결정할 수 있으니 더 나은 상태가 되었을 것이다.

페이지 진입시 보여지는 컴포넌트를 금새 파악할 수 있다

 

아래 구조가 되면서 컴포넌트 트리 구조를 버리게 되었지만(구조화가 깨져버렸다), 페이지에 대한 맥락은 파악하기 유리해졌다. PageA에서 보여지는 C2 컴포넌트의 타이틀을 변경하기 위해 컴포넌트를 타고 돌아다닐 필요가 없다. 만약 상태에 따라 PageA에서 B1, C2 컴포넌트가 보여질 수 있다고 하더라도 usePageFooter에 넣는 파라미터를 보면 금새 찾을 수 있을 것이다.

 

불필요하게 단단한 코드를 지양해야 한다

여기서 단단한 코드란 변화에 유연하지 않은 코드다. 타입 가드와 상속을 이용하는 인터페이스 정의를 예로 들 수 있다. 물론 이것들은 훌륭하고 유용한 기술이다. 하지만 개발 단계에서 정말 필요할지 고민해볼 필요가 있다. 예를 들어 타입 가드를 이용해 아주 명확하게 나누어진 타입이 있다고 하자.

얼핏 보기에 적당해 보인다

 

특정 타입에 대한 응답에 필드를 추가하는 것은 쉽다. 하지만 그 타입을 상속했기 때문에 문제가 복잡해진다. 특히 다중 상속이 들어간 순간 특정 타입에 대해서만 변경하기는 아주 어려워진다. 위 예시에서 ICommonResponse의 job이 필수 값이 되었다면 어떨까? 단순히 ICommonResponse 필드에서만 변경해도 될까? 하지만 이것이 외부로부터 받는 값이라 job을 제공받지 못해서 실패한다면? JoinFailure에서 상속받는 ICommonResponse에서 job필드를 Optional로 바꿔도 될까? 아니, job을 제공받지 못해서 실패했으니 type도 추가될 것이다. 그럼 그 type에 대응하기 위한 새로운 interface를 정의해야 할지도 모른다. 잠시 생각해보면 답은 찾을 수 있다. 그런데 그 답을 코드로 작성한 결과물이 다른 개발자가 보기에 잘 만든 코드일지는 장담할 수 없다.

 

개인적으로는 이런 문제를 피하기 위해 초기 개발, 그러니까 QA에 들어가기 전까지는 바보같은 타입 나열이 낫다고 생각한다. TS답지도 않고 무의미한 선언이 많아진다. 하지만 변화에는 유연해진다. 이렇게 개발이 진행 되고 자리잡은 이후 리팩토링하면 되지 않을까? 개발 도중에 JS가 자리잡을 수 있었던 그 유연성을 포기할 필요는 없다.

type에 string도 들어간게 정말 이상해보인다. 하지만 유연하다

 

조직원의 평균 레벨에 맞는 코드를 작성해야 한다

이것은 이전 회사에서 느꼈던 요소다. 팀원 모두가 올라운드 전문가는 아니다. 각자의 전문 분야가 있을 뿐이다. 내가 잘 아는 부분이 있다고 해서 그걸로 멋지게 해결해내는 것은 도움이 되지 않을 수 있다. 변경이 필요할 때 항상 내가 작업할 수 있을지 모르기 때문이다. 손이 비는 다른 개발자에게 맡기는 순간 작업 시간은 배로 소요된다. 관리자에게 있어서도, 실무자에 있어서도 이런 코드는 골칫덩이다.

스카우터가 터질 것 같은 개발력

 

이 부분의 가장 대표적인 것은 함수형 프로그래밍일 것이다. ramda같은 라이브러리를 이용하면 짧은 코드로 멋지게 해결해낼 수도 있지만 유지보수에 있어서 훨씬 더 긴 시간을 소요하게 할 수도 있다. 그런 코드에 익숙하지 않다면 앞서 말했던 맥락 파악에 어려움이 생기기 때문이다. 특히나 연차가 낮거나 전공자 출신이 아닌 개발자가 많을 수 있는 FE 개발은 더욱 그렇다.

 

중복에 대해 알려야 한다

중복된 코드를 만드는 것에 두려움을 가져서는 안된다. 하지만 중복되는 내용이 있다는 것을 알려야 한다. 특히 dirty and paste를 지향할수록 생산성이 좋아지는 개발 단계에서는 더 중요하다. 결점을 찾아 어느 한 부분만 고쳐놓았지만, 단순히 붙여넣기 해놓은 코드가 너무나 적절하고 완벽해서 릴리즈 직전까지 아무도 문제를 찾지 못할 수 있다. 이런 경우 다른 개발자는 찾기가 더더욱 어려워진다. 이미 결점을 해결해놓은 부분을 가지고 열심히 고민하고 있을지도 모른다.

이렇게 해두면 바로 인지할 수 있다

 

이런 상황을 방지하기 위해 미리 유사하거나 같은 코드가 어느 부분에 있는지 적어둘 필요가 있다. 코드를 고치는 것만이 중요한게 아니다. 코드를 고칠 수 있게 유도하는 것도 중요하다.

 

이유를 알 수 있어야 한다

개발자는 요구에 따라 바보가 되어야 할 수도 있다. 이렇게 바보가 되었을 때, 왜 그렇게 했는지에 대해 반드시 적어야 한다. 불의를 참지 못하는 개발자들은 이런 코드를 고치고 싶어하는데, 고치는 순간 장애가 발생한다. 간단한 주석이 앞으로의 장애를 막는 코드가 될 수도 있다.

 

이번 프로젝트에서는 웹뷰를 닫을 때 불필요한 다이얼로그를 호출하도록 한 코드가 있다. 대상이 되는 앱은 웹뷰가 닫힐 때 약 300ms 정도의 애니메이션이 있고, 애니메이션이 종료 되었을 때 웹뷰가 완전히 닫힌 상태가 된다. 평소에는 아무런 문제가 없는 동작이지만 웹뷰가 여러개 쌓여있을 때 문제가 발생했다. 상위 웹뷰가 닫히면 아래에 있던 웹뷰가 특정 응답을 수신하고 곧바로 닫히는 구현이 있었다. 포인트는 상위 웹뷰가 닫히는 애니메이션이 동작하고 있을 때, 하위 웹뷰가 활동 가능한 상태가 된다는 점에 있었다.

앱이 현재 보이는 창을 닫도록 개발된 모양이다

 

하위 웹뷰에서는 필요한 로직을 수행하고 웹뷰를 닫는데 상위 웹뷰가 아직 닫히지 않았으므로 상위 웹뷰를 닫고자 하는 것이다. 닫히는 애니메이션이 동작하고 있던 웹뷰에 다시한번 닫는 요청이 들어가고, 하위 웹뷰는 닫히지 않아 더 이상 동작이 이어지지 않게 되어버린다. 근본적인 문제를 해결하기에는 시간이 부족하여 하위 웹뷰에서 다이얼로그를 띄운 후 버튼을 눌렀을 때 웹뷰를 닫도록 수정했다. 얼핏 보기에 아무 의미 없는 메시지 다이얼로그이지만 꼭 필요한 요소가 되어버린 것이다. 이런 이상한 히스토리가 있는 코드를 설명 없이 알 수 있을까?

 

 

마치며

사실 위 모든 것을 해결할 수 있는 방법은 더 꼼꼼한 코드 리뷰다. 하지만 그건 너무나 꿈같은 이야기다. 이번 프로젝트를 진행하며 코드 리뷰의 중요성을 뼈저리게 느꼈지만 실현하기에는 쉽지 않아 보인다. 이렇게 일단락 지어둔다면 혹여나 누군가 도움을 요청했을 때 이런걸 생각했었다며 전달해줄 수 있을 거라 기대한다.