
리액트라는 UI 라이브러리를 사용한 지 3년이 넘었지만, 그동안 ‘나는 리액트라는 라이브러리를 잘 사용하고 있는 걸까?’ 혹은 ‘리액트를 잘 사용한다는 기준은 어떤걸까?’ 에 대해 깊이 고민해 본 적은 없었다. 그러던 중 우연히 유튜브에서 한 영상을 보게 되었고, 다시 한 번 내 접근 방식을 돌아봐야겠다는 생각이 들었다. 이번 포스팅에서는 그동안의 경험을 되돌아보고, 리액트를 효과적으로 사용하기 위한 몇 가지 중요한 포인트들을 정리해 보고자 한다.
어플리케이션이 복잡해지면 상태의 양도 많아지고, 이를 전역에서 관리하는 것이 어려워질 수 있다. 특히, 계층 구조가 깊은 컴포넌트 트리에서 최하단 컴포넌트에서만 사용되는 상태를 상위 부모 컴포넌트에서 관리하거나, 외부 상태 관리 라이브러리를 과도하게 사용하는 방식은 바람직하지 않다.
상태 관리에 접근할 때는 다음의 세 가지 원칙을 고려할 수 있다.
간단하거나 지역적인 상태는 리액트 내장 API인 useState, useReducer 등을 사용하여 충분히 관리할 수 있다.
불필요하게 외부 상태 관리 라이브러리를 도입하면 오히려 코드의 복잡도가 증가할 수 있으므로, 상태의 복잡도와 범위를 고려하여 적절한 도구를 선택하는 것이 중요하다.
여러 컴포넌트에 걸쳐 공유되거나 복잡한 로직이 필요한 상태는 관리가 어려워지므로, 상태를 분리하고 모듈화하는 것이 중요하다.
컴포넌트 간 데이터 흐름을 명확하게 하기 위해 상태를 분리하거나 컨텍스트(Context) API 등을 적절히 활용하는 것이 좋다.
또한, 필요한 경우 외부 상태 관리 라이브러리를 도입하여 상태의 변화와 흐름을 체계적으로 관리하는 것이 바람직하다.
서버에서 연동되는 데이터는 '서버 상태'로 구분하여 관리하는 것이 좋다.
이 경우, SWR이나 Tanstack Query와 같은 라이브러리를 사용하면 비동기 데이터 처리, 캐싱 및 재검증 등의 작업을 효과적으로 수행할 수 있다.
프론트엔드 디자인 패턴은 MVC, MVP, MVVM, Component Pattern, Flux Pattern 등 굉장히 다양하다. 여러 디자인 패턴은 애플리케이션의 복잡도와 요구사항에 따라 선택할 수 있지만, 우선은 디렉토리 구조를 잘 만드는 것이 장기적인 유지보수와 협업에 결정적인 영향을 미친다. 디자인 패턴과 마찬가지로 디렉토리 구조에도 정답은 없지만, 아래의 두 가지 원칙을 지키는 것이 좋다.
파일과 컴포넌트가 한 곳에 모두 몰리면 파일 간 의존 관계 파악과 수정이 어려워진다. 계층적으로 구성할 경우, 기능별 또는 도메인별로 파일을 분리하여 모듈화할 수 있다. 이렇게 하면 특정 기능을 수정하거나 확장할 때 관련 파일만 집중적으로 살펴볼 수 있어 유지보수와 협업이 용이해진다. 또한, 팀 내 새로운 개발자들이 전체 프로젝트 구조를 빠르게 파악할 수 있다는 장점이 있다.
“서랍장이 정리되어 있으면 원하는 물건을 빠르게 찾을 수 있다”는 비유처럼, 명확한 규칙과 일관된 명명 규칙을 적용한 디렉토리 구조는 파일의 위치를 예측 가능하게 만든다. 파일 위치가 명확하게 정해져 있으면 신규 개발자나 협업하는 팀원들이 프로젝트에 빠르게 적응할 수 있고, 코드 가독성과 관리 효율성이 크게 향상된다.
리액트 프로젝트 역시 "읽기 좋은 코드"가 유지보수와 협업에 있어 핵심 요소이다. 코드의 가독성이 높으면 의도를 명확하게 파악할 수 있어 버그를 줄이고 생산성을 향상시킬 수 있다. 리액트와 같은 컴포넌트 기반 라이브러리도 예외는 아니며, 클린 코드를 지향하는 원칙들이 그대로 적용된다.
읽기 좋다는 것은 결국 가독성과 예측 가능성을 의미한다. 즉, 코드의 의도가 명확하게 드러나고, 동일한 규칙이 일관되게 적용되면 코드의 수정과 확장이 쉬워진다. 여러 실무 사례와 오픈소스 프로젝트에서 볼 수 있듯이, 네이밍 규칙과 파일 구조가 일관적이면 신규 개발자나 협업하는 팀원들이 코드를 빠르게 이해할 수 있다. 읽기 좋은 코드를 구성하기 위한 네이밍 규칙은 다음과 같다.
디렉토리와 파일은 프로젝트의 구성 요소를 나타내므로, “컴포넌트”, “페이지”, “서비스”처럼 고정된 개념을 드러내는 명사로 작성하는 것이 좋다. 예를 들어, Button.js나 UserProfile 폴더처럼 명사 형태로 작성하면, 해당 파일이나 폴더가 무엇을 의미하는지 명확해진다. 이는 클린 코드 원칙 중 “의도를 분명하게 드러내라”는 부분과도 일치한다.
함수는 어떤 행동을 수행하므로, 함수 이름에 동사를 포함하여 해당 함수의 동작을 예측 가능하게 만드는 것이 중요하다. 예를 들어, getUserInfo(), updateProfile() 등은 함수가 무엇을 하는지 바로 알 수 있게 해준다. 이런 방식은 코드 리뷰와 유지보수 시에도 큰 도움이 된다.
변수는 데이터를 나타내므로, 해당 변수에 담긴 값의 역할을 쉽게 파악할 수 있도록 명사형을 사용하는 것이 바람직하다. 예를 들어, userList, profileData와 같이 작성하면, 해당 변수가 무엇을 의미하는지 직관적으로 이해할 수 있다.
리액트 컴포넌트는 결국 하나의 함수로 동작한다. 따라서 컴포넌트를 설계할 때에도 일반적인 함수 작성 원칙, 즉 단일 책임 원칙(SRP)을 적용할 수 있다. 여러 개발 서적과 커뮤니티에서도 “하나의 컴포넌트는 하나의 역할만 수행해야 한다”는 점을 강조한다. 이는 코드 가독성을 높이고, 유지보수 및 재사용성을 극대화하는 데 중요한 역할을 한다.
단일 책임 원칙에 따르면, 한 컴포넌트가 여러 가지 역할을 수행하면 코드의 복잡도가 높아지고, 변경 사항이 생길 때 다른 기능에까지 영향을 미치기 쉽다. 예를 들어, UI의 표현과 비즈니스 로직을 한 컴포넌트에 모두 넣으면, 해당 컴포넌트를 이해하기 어려워지고 테스트 또한 복잡해진다. 따라서 컴포넌트는 명확하게 어떤 UI 요소나 기능을 담당하는지 표현되어야 한다. 이러한 원칙을 기반으로 컴포넌트를 작게 분리하면 재사용성과 독립성을 확보할 수 있다.
만약 컴포넌트 코드가 길어져 스크롤이 길어지는 상황이라면, 이는 해당 컴포넌트에 너무 많은 책임이 몰렸다는 신호이다. 코드가 길어지면 가독성이 떨어지고, 추후 수정 시 오류가 발생할 가능성이 높아진다. 이런 경우에는 하위 컴포넌트로 분리하여 각 컴포넌트가 담당하는 역할을 명확히 하고, 코드를 모듈화하는 것이 좋다.
컴포넌트를 작게 분할하면, 각 컴포넌트 간 의존성이 줄어들어 한 컴포넌트에 변경이 생겨도 다른 컴포넌트에 미치는 영향을 최소화할 수 있다. 예를 들어, 최하단(UI의 가장 작은 단위) 컴포넌트에 수정이 발생하더라도, 해당 컴포넌트만 재렌더링되거나 수정되므로 전체 애플리케이션에 미치는 영향이 줄어든다. 또한, 작고 독립적인 컴포넌트는 재사용성이 높아 여러 UI에서 동일한 컴포넌트를 활용할 수 있으므로, 코드의 중복을 줄이고 개발 생산성을 향상시킨다.
리액트 프로젝트에서도 DRY(Don't Repeat Yourself) 원칙을 준수하는 것은 유지보수성과 확장성을 높이는 데 매우 중요하다. 중복 코드가 많으면 동일한 수정 사항을 여러 군데 반영해야 하고, 이는 버그 발생 위험을 높이며 코드의 일관성을 해칠 수 있다.
예를 들어, 약간 다른 역할을 하는 버튼 컴포넌트가 2개 있다면, 두 컴포넌트를 따로 관리하기보다는 공통된 기능과 스타일을 추출하여 하나의 범용 버튼 컴포넌트를 만드는 것이 좋다. 이렇게 하면 나중에 버튼의 스타일이나 기능을 변경할 때 한 군데만 수정하면 되어 유지보수가 쉬워진다. 또한, 상속 구조나 mixin, 또는 컴포지션을 활용해 공통 로직을 분리하면 코드 중복을 더욱 효과적으로 줄일 수 있다.
여러 컴포넌트에서 동일한 비즈니스 로직이나 이벤트 처리가 반복된다면, 이를 커스텀 훅으로 분리하여 관리하는 것이 바람직하다. 커스텀 훅을 사용하면 로직을 한 곳에 집중시킬 수 있어 코드의 일관성을 유지하고, 테스트 및 유지보수가 용이해진다. 리액트 공식 문서와 다수의 커뮤니티 자료에서도 커스텀 훅을 활용해 공통 로직을 재사용하는 방법이 권장되고 있다.
복잡한 애플리케이션에서는 특정 부분에서 성능 병목현상이 발생할 수 있다. 이러한 문제를 해결하기 위해서는 우선 크롬 개발자 도구나 React Profiler 등으로 어느 부분에서 불필요한 리렌더링이나 과도한 계산이 일어나는지 진단해야 한다. 실제로 React 공식 문서와 여러 성능 최적화 관련 자료에서도, 진단 없이 최적화 API를 무작정 적용하는 것보다 문제를 정확히 파악한 후 적절히 적용하는 것이 중요하다고 강조한다
리액트는 React.memo, useMemo, useCallback 등 다양한 최적화 API를 제공한다. 이들 API는 컴포넌트의 불필요한 리렌더링을 방지하거나, 값의 재계산을 줄이는 데 효과적이다. 하지만 이러한 API들은 실제로 병목현상이 발생하는 부분에 한해서 적용해야 하며, 모든 컴포넌트에 무작정 적용하면 오히려 코드의 복잡도를 높이고 유지보수를 어렵게 만들 수 있다. 성능 진단 도구로 문제가 되는 부분을 확인한 후에 최적화 API를 적용하는 것이 바람직하다.
프론트엔드에서 가장 많은 시간을 소비하는 부분 중 하나는 서버와의 데이터 통신이다. 데이터 페칭에 소요되는 시간을 줄이기 위해, SWR이나 React Query와 같은 라이브러리에서 제공하는 캐싱 기능을 활용하면, 이미 받아온 데이터를 재사용함으로써 네트워크 요청을 최소화할 수 있다. 이는 사용자에게 보다 빠른 응답성을 제공하며, 전체 애플리케이션의 성능 최적화에 큰 도움이 된다
프론트엔드에서 사용자 경험(UX)은 단순히 디자인만이 아니라, 앱이 얼마나 빠르고 부드럽게 동작하는지에 크게 좌우된다. 특히, 복잡한 어플리케이션에서는 데이터 통신, 렌더링, 업데이트 과정에서 발생하는 지연이나 “끊김” 현상이 사용자 불만으로 이어질 수 있다. 이에 따라, 개발자는 성능 진단 도구와 최적화 API를 활용하여 UX 성능을 개선해야 한다.
여러 API 호출이 필요한 경우, 직렬(순차적) 요청과 병렬 요청의 차이를 이해하고 적절히 활용하는 것이 중요하다.
Promise.all 등의 기법을 사용해 동시에 실행하면 전체 응답 시간을 단축할 수 있다.사용자에게 진행 상황을 시각적으로 전달하는 것은 끊김없는 UX를 위한 핵심 요소다.
로딩 스피너 또는 스켈레톤 UI: 데이터가 로드되는 동안 해당 영역에 로딩 인디케이터를 표시하면, 사용자는 “작업이 진행 중”임을 인지하여 불안감을 줄일 수 있다.
Fallback UI와 Suspense: 리액트의 <Suspense> 컴포넌트를 활용하면, 로딩 중인 콘텐츠 대신 가벼운 로딩 인디케이터를 보여줄 수 있다. (React 공식 문서의 Suspense 를 참고하면, fallback 프로퍼티를 통해 로딩 중 대체 UI를 지정하는 방법을 확인할 수 있다. Suspense 공식문서 참고)
리액트 18부터 도입된 useTransition과 useDeferredValue 훅은, UI 업데이트를 비동기적으로 처리하여 사용자 인터랙션을 블록하지 않도록 돕는다.
useTransition: 상태 업데이트를 “Transition”으로 표시하여, 긴급하지 않은 업데이트가 백그라운드에서 처리되도록 함으로써 이미 화면에 표시된 콘텐츠가 갑자기 사라지지 않게 한다.
useDeferredValue: 값 업데이트에 약간의 지연을 주어, 입력과 같이 빠르게 변화하는 데이터에 대해 부드러운 업데이트를 구현할 수 있다.
서버 사이드 렌더링(SSR)을 사용하는 경우, Suspense를 적절히 활용하면 초기 로딩 시 사용자에게 빠르게 콘텐츠를 전달하면서도, 필요한 데이터가 준비될 때까지 fallback UI를 표시할 수 있다.
앞서 소개한 여러 원칙들(상태 관리, 아키텍처 설계, 클린 코드, 컴포넌트 분리, 중복 제거, 최적화, 끊김없는 UX) 모두 중요한 요소이지만, 결국 가장 중요한 것은 회사의 핵심 비즈니스 요구사항에 신속하게 대응할 수 있는 구조를 유지하는 것이다. 즉, 변화하는 비즈니스 환경에 맞춰 코드를 유연하게 수정하고 확장할 수 있도록 하는 것이 우선이다.
비즈니스 환경은 끊임없이 변화하며, 이에 따라 서비스 요구사항도 달라진다. 따라서 코드와 아키텍처는 이러한 변화에 빠르게 대응할 수 있어야 한다.
자주 변경되는 기능이나 모듈을 미리 파악하고, 해당 부분을 독립적인 모듈이나 컴포넌트로 분리해 두면, 비즈니스 요구사항이 변경되었을 때 해당 부분만 집중적으로 리팩토링할 수 있어 전체 시스템에 미치는 영향을 최소화할 수 있다.
이러한 구조적 접근은 장기적으로 유지보수 비용을 낮추고, 빠른 시장 대응력을 갖추는 데 큰 도움이 된다.
이번 포스팅을 통해, 내가 리액트를 사용하면서 무의식적으로 따랐던 여러 원칙들을 다시 한 번 상기하게 되었다. 이 원칙들만 잘 지키면 SPA 프레임워크를 기반으로 한 협업에서도 큰 불편함 없이 개발할 수 있다고 생각한다. 이 글을 읽는 여러분도 각자의 방식으로 리액트를 더욱 잘 활용해보시길 바란다.
https://www.youtube.com/watch?v=RzbBeRSnvRo&t=204s
https://wikidocs.net/257837
https://ko.react.dev/reference/react/Suspense
https://ko.react.dev/reference/react/useTransition
https://frontend-fundamentals.com/code/