H Management System 프로젝트 회고

이진혁·2023년 6월 28일

들어가며

짤 한 장으로 설명 가능한 프로젝트 회고.

이번 프로젝트는 이전 회사에서 진행한 작업을 개선하고, Next.js와 TypeScript를 도입하여 효율적인 웹 애플리케이션으로 재구축하는 작업이었다.

해당 프로젝트는 기업에서 운용 중인 서빙 로봇의 데이터를 관리하고 통제하는 관제시스템 성격의 서비스였다.

회사의 요구 사항을 충족시키기 위해 기능 구현에 주력했던 결과, 성능 최적화나 프로젝트 설계 구조와 같은 다른 측면을 고려하는 시간이 부족했다.

결국 리팩토링할 시간도 없이 인턴 생활이 마무리되었고 프로젝트 또한 전반적인 수정이 필요한 상태로 남게 되었다.

프로젝트의 폴더 구조와 파일에 대해 스스로 납득이 가지 않았으며 가독성을 해치고 중복된 불필요한 코드가 존재했다. 또한 성능 개선이 필요한 페이지도 있었다.

아쉬움이 많이 남은 프로젝트였고 개발자로서의 본분을 다하지 못했다는 의식에 사로잡혀 퇴사 후 회사의 허락을 구해 프로젝트를 처음부터 끝까지 다시 구축하면서 새롭게 개선하기로 결정했다.

프로젝트를 진행하며 주어진 상황에서 최선의 방법을 도출하고 해결하는 과정에서 개발자로서 많이 성장한 포인트가 있었고, 처음 마주한 어려운 문제들과 다양한 상황에 직면하게 되면서 겪었던 성장과 경험을 기록으로 남기려 한다.

성능 최적화에 집중하기

프로젝트를 다시 뜯어 고치면서 clean한 코드와 가독성은 당연한 부분이었지만, 가장 집중해야 할 부분은 성능 최적화였다.

서비스의 성격 상 로봇에서 발생하는 에러나 데이터 로그를 관리해야 했기 때문에 클라이언트 단에서 처리해야 할 데이터 양이 상당했기 때문이다.

이전 프로젝트에서는 성능 최적화를 고려하지 않고 기능 구현에만 초점을 맞추었기 때문에 느린 페이지 진입 속도 등 사용자 경험에 영향을 미치는 이슈가 발생했다.

이번에 다시 프로젝트를 진행하며 성능 최적화에 정말 많은 고민을 쏟아 부었는데 그 중 몇 가지를 적어보려한다.

불필요한 통신 횟수 최대한 줄이기

당시 프로젝트 기능 요구 사항 중 실시간으로 데이터를 받아와 클라이언트에서 뿌려주는 기능이 있었다.

서빙 로봇을 주력으로 삼은 기업의 특성상 로봇에서 발생하는 에러는 굉장히 치명적이기 때문에 빠르게 체크해야 했기 때문이다.

해당 기능을 구현하기 위해 Web Socket이나 SSE와 같은 실시간 양방향 통신 기술을 떠올렸고, 실시간 데이터 통신 기능을 구현한 경험이 있었기 때문에 무난히 구현할 수 있는 기능이라 생각했다.

하지만 백엔드의 개발 리소스 부족으로 인해 위에 언급한 실시간 통신 기술을 쓸 수 없는 상황이 발생했다.

당시 회사 개발 팀원 중 한 분이 사내 프로젝트에서 개발했던 기능 중에 실시간으로 데이터를 받아와 뿌려주는 작업을 폴링을 통해 1초 마다 데이터를 패칭하는 방식으로 구현했다고 말씀해주셨다.

처음에는 다른 방법이 없다고 생각하여 해당 방식을 그대로 수용하여 기능을 구현했지만, 새롭게 뜯어 고치게 되면서 이는 서버의 부하를 일으킬 여지가 충분하다고 생각했고 절대 최선의 방법이 아니라고 판단했다.

실시간 통신 기술을 사용하지 못 하는 상황에서 결국 나에게 주어진 건 API 한 줄.

정말 많은 고민 끝에 내린 결론은

데이터를 일정 주기마다 패칭해오는 폴링 방식으로 구현하되, 패칭해오는 주기를 다양한 변수와 상황에 맞게 최적의 주기를 동적으로 할당해주는 작업을 진행하여 최적화를 진행했다.

자세한 내용은 React-query refetchInterval 동적으로 할당하기에서 확인 할 수 있다.

위의 과정을 통해 1초마다 데이터를 패칭해오던 기존 방법에서 현재 매장 상황과 에러의 시간 차를 고려해 동적으로 주기를 할당해줌으로써 서버의 부하를 조금이나마 줄일 수 있었다고 생각한다.

하지만 이 방법은 내가 처한 상황에서 내가 선택할 수 있는 최선의 결정이라고 생각하며, 어찌되었건 폴링 방식은 서버의 부담을 주게 되므로 리소스가 충분하다면 Web Socket이나 SSE를 사용하는 것이 옳은 방법이라고 생각한다.

효과적으로 데이터 리스팅하기

당시 로봇에서 발생하는 에러 로그들을 리스팅하는 기능을 구현하면서 문제가 발생했다.

서버로부터 받아오는 에러 로그 데이터의 양이 많아서 한 번에 모든 데이터를 리스팅하면 페이지 진입 속도가 느려지는 이슈가 있었다.

이를 해결하기 위해 순차적으로 데이터를 요청하는 무한스크롤 형식으로 구현하려 했지만 백엔드 쪽의 개발 리소스 부족으로 인해 데이터의 페이지네이션 작업을 할 수 없는 상황이 발생했다.

부끄럽게도 당시에는 다른 방법이 없다 생각하여 그대로 데이터를 리스팅했었고 당연하게도 페이지 진입 속도가 느려지는 이슈가 발생했다.

하지만 이번에 새롭게 고치면서 모든 데이터를 한 번에 리스팅하는 방법 대신, 데이터를 한 번에 모두 받아오되 처음에는 10개 정도의 데이터만 렌더링하고 스크롤을 내리면 나머지 데이터를 조금씩 잘라서 붙여 넣는 작업을 진행해 최적화를 진행하기로 결정했다.

초기 구현 방식은 addEventListener와 scroll 이벤트를 통해 데이터를 리스팅하고 debounce를 이용해 event 이슈를 최소로 줄였다.

하지만 위의 방식은 scroll event를 감지하여 구현했기 때문에 결국 자바스크립트의 메인 엔진에서 실행이 되어야하므로 많은 부하가 걸린다.

아무리 웹 성능이 좋아졌다고 하더라도 성능 저하의 우려가 있는 방식은 최대한 지양해야 하기 때문에 Intersection Obsever API를 활용해 구현하기로 결정했다.

자세한 기능 구현 내용은 Intersection Observer API를 활용한 무한스크롤 구현 에서 확인할 수 있다.

위의 과정과 함께 초기 번들 크기를 줄이고 페이지 진입 속도를 개선하기 위해 모듈을 동적으로 로드하는 Dynamic import를 에러 로그들을 리스팅하는 컴포넌트에 적용하여 진입 속도 평균 1.8s ⇒ 평균 1.2s 로 단축시킬 수 있었다.

확장성과 유지보수에 용이한 구조로 설계하기

회사에 다니면서 크게 깨달은 점 중 하나는 바로 프로젝트를 개발하는 동안 비즈니스 요구사항이 자주 변할 수 있다는 것이다.

또 프로젝트의 수명 주기가 길 경우, 초기에 확장성과 유지보수를 고려한 구조로 설계하는 것이 장기적으로 유리하다는 것을 느꼈다.

단기적으로 진행했던 프로젝트 경험만 있던 당시의 나는 확장성이나 유지보수를 고려하지 않고 기능 구현에만 집중했기 때문에 자주 변하는 비즈니스 요구사항에 대응하기 어려웠고

결과적으로 작업했던 결과물을 몇 번이나 갈아 엎는 비효율적인 상황이 발생했다.

성능 최적화 다음으로 많은 리소스를 쏟아 부은 부분이라 프로젝트를 어떻게 확장성과 유지보수에 용이한 구조로 설계했는지 적어보려 한다.

재사용 가능한 로직 분리하기

당연하게도 가장 먼저 진행한 부분은 바로 재사용 가능한 로직을 분리하는 것이었다.

재사용 가능한 로직을 분리하면 비슷한 기능을 하는 부분에서 중복된 코드를 피할 수 있으며 코드 작성 시간이 단축되고, 개발 생산성이 향상되는 등의 이점을 가진다.

또 여러 곳에 흩어져 있는 유사한 로직을 개별적으로 수정하는 번거로움을 피할 수 있으며, 변경사항에 대한 오류가 줄어든다.

나의 경우 API, 상태 관리, 애니메이션 등과 같은 로직을 재사용 가능한 모듈로 분리하여 중복 코드를 줄이고 코드 유지보수성을 향상시켰으며

복잡하거나 재사용될 수 있는 함수는 성격과 쓰임에 따라 custom hook 또는 utils 함수로 구분해서 모듈화를 진행했다.

이로 인해 개발 속도가 크게 향상되었고 코드의 효율성과 품질을 높이는데 크게 기여를 했다고 생각한다.

객체 매핑 활용하기

서버로부터 넘어오는 데이터가 클라이언트 단에서 쓰이기 적합하지 않은 경우가 많아 데이터를 다시 가공해야하는 경우가 발생했다.

예를 들어, 어느 api에서는 '향동 노리 배달쿡'이라는 이름으로 데이터가 넘어오는 반면, 다른 api에서는 '노리 배달쿡 향동점' 이런식으로 데이터의 일관성이 지켜지지 않았다.

데이터 구조의 변화에 유연하게 대응할 수 있도록 객체 매핑을 활용하기로 결정했는데,

반복적인 데이터 변환 로직을 모듈화하여 재사용 가능하게 만들어주어 비슷한 변환 작업을 여러 곳에서 반복적으로 구현하지 않아도 되므로, 불필요한 코드의 양이 줄어들었고 개발 생산성이 향상되었다.

다른 방식보다 깔끔하고 가독성이 좋은 건 덤이다.

상수 데이터를 함수형으로 관리

const createData = (id : number, state : string){ return { id, state } }
const DATA = [createData(0, “foo”), createData(1, “bar”),] 

나는 개발할 때 같은 형식의 구조를 가지는 데이터를 렌더링 시 일일이 하드 코딩하는 것보다 상수 데이터를 선언하고 깔끔하게 map으로 처리하는 방식을 선호한다.

위와 같이 함수를 사용하여 상수 데이터를 생성하면, 해당 데이터의 구조와 의미를 명확하게 표현할 수 있기 때문이다.

함수 이름과 파라미터를 통해 데이터의 의도를 더 잘 이해할 수 있으며, 코드 가독성이 향상되고 TS의 파라미터의 타입 지정을 통해 상수 데이터를 생성할 때 발생할 수 있는 오류를 사전에 방지할 수 있다는 이점을 가진다.

또한 상수 데이터를 하나의 함수에서 관리하므로 나중에 데이터를 수정해야 할 때 함수만 수정하면 되므로 유지보수가 편리해지기 때문에 이번 프로젝트를 진행하면서 적용시켜주었다.

마치며

이번 프로젝트는 나에게 있어 정말 중요하고 개발자로서 성장할 수 있었던 포인트가 매우 많았던 프로젝트라고 생각한다.

특별한 기술적 챌린지나 난이도가 있었던 프로젝트는 아니었지만, 기능 구현에만 초점을 맞춘 이전의 프로젝트와는 달리 이번에는 성능 최적화와 확장성, 유지보수에도 신경을 써야 했기 때문에 더 많은 고민과 노력이 필요했다.

프로젝트 전반에 걸쳐 내가 선택한 결정에 대해 스스로 납득할 만한 근거를 찾기 위해 노력했으며 주어진 상황에서 최선의 방법을 도출해내기 위해 정말 많은 시간을 할애했다.

덕분에 조금이나마 더 나은 선택지에 다가갈 수 있었고 오만하다고 생각할 수 있겠지만 결과적으로 사용자 입장에서, 개발자 입장에서 납득이 갈만한 프로젝트로 마무리 지었다고 생각한다.

물론 이 프로젝트가 완벽하다고 말할 수는 없지만 이번에 겪었던 노력과 성장이 이후에 맡을 프로젝트에서 더 나은 선택과 결정을 내릴 수 있는 양분이 되었다고 확실하게 말할 수 있다.

정말 중요하고 의미있는 프로젝트였기에 앞으로의 개발자 생활에서도 이번 프로젝트에서 얻은 경험과 교훈을 가슴에 새기며 끊임없이 발전하고 성장해 나가고 싶다.

내가 지향하는 개발자의 모습에 한 발자국 더 다가간 것 같아 기분이 좋다!

profile
개발 === 99%의 노력과 1%의 기도

0개의 댓글