프론트엔드 상태 관리에 대한 생각 - 무엇인가, 왜 하는가, 어떻게 잘 하는가

2021년 12월 4일

주의 ⛔️

  • 이 글이 작성된지 2년이 넘었어요.
  • 누구나 숨기고 싶은 흑역사가 있답니다.

학교에서는 백엔드 개발 밖에 할줄 몰랐던 제가, 작년에 회사에 입사한 후 무턱대고 프론트엔드 개발을 맡게된지도 벌써 약 1년이 지났습니다.

당장 HTML CSS 조차도 몰라서 퇴근 후엔 열심히 개인 프로젝트를 하면서 공부했던 기억이 납니다. (지금도 잘 모르는건 마찬가지인 것 같지만요)

팀 내에 프론트엔드 개발자가 저를 포함해 두 명이 있었는데, 올해 4월경 한 분께서 이직하시는 바람에.. 어쩌다 배우게 된 프론트엔드를 혼자서 개발하게 되는 위기(?)까지 겪기도 했죠. (어쩌다보니 아직까지 해내고 있긴 합니다)

사실 저희 팀은 모두가 프론트엔드, 백엔드를 동시에 개발할 수 있지만, 작업을 맡는 비율이 달랐습니다. 제 경우엔 전체가 10이라고 하면 프론트엔드가 8, 백엔드가 2 정도였습니다.

그렇게 경력의 대부분이 프론트엔드 개발로 채워져 갔지만, 그에 비해 제가 프론트엔드 개발자로써 기술적인 이해도를 얼마나 갖고 있느냐는 항상 고민이 되는 문제였습니다.

그 중에서도 가장 어렵게 다가왔던 주제가 '상태 관리'였던 것 같습니다. 당장 누군가 "프론트엔드에서 '상태'란 무엇이고, '상태 관리'란 뭘 하는걸까요?" 라고만 물어봐도 말문이 턱 막혔었죠.

그래서 이 글에서는 약 1년간의 프론트엔드 개발을 통해 제가 느꼈던 '상태' 그리고 '상태 관리'의 의미와, '상태 관리'를 하는 이유, 그리고 어떻게 해야 '상태 관리'를 잘할 수 있는지에 대한 생각들을 정리해보려고 합니다.

상태와 상태 관리의 의미

프론트엔드 개발에서 상태란 무엇일까요?

저는 "UI에 영향을 줄 수 있는 모든 데이터"라고 정의하고 싶습니다.

다음은 제가 생각하는 상태의 예시입니다.

  • 팔로워 수
  • 장바구니에 담긴 상품의 리스트
  • 총 결제 금액
  • 카드사 할인 혜택
  • 현재 열려있는 모달
  • 로그인된 유저의 정보
  • 유저가 현재 보고있는 페이지
  • 어떤 UI를 보여줄지에 대한 여부
  • 화면의 가로 세로 크기
  • input의 입력 값
  • ...

이렇게 UI에 영향을 주는 데이터, 즉 상태의 종류는 정말 셀 수 없이 많습니다. 그리고 이러한 상태들은 가만히 있지 않고 변화합니다.

예를 들어, 키보드 입력을 했을 때 '입력값'은 변화합니다. 또한 모달을 닫았을 때 '현재 열려있는 모달'은 변화합니다.

그리고 좀 구체적인 예시를 들고 싶은 상태가 있는데, 바로 '어떤 UI를 보여줄지에 대한 여부'입니다.

알림 센터를 예시로 들어봅시다. 우선 서버로부터 알림 리스트를 로딩해야합니다. 알림 리스트가 로딩되는 동안에는 유저에게 Skeleton UI를 보여줘야겠죠. 로딩이 끝나면 알림 리스트를 UI에 렌더링합니다. 또는 로딩중에 에러가 났다면 에러 페이지로 이동하거나, Skeleton UI를 계속 보여주고 다시 데이터를 요청합니다.

이 때, '알림이 언제 온건지'를 UI에 보여주려고 합니다. 그냥 '날짜/시/분/초'로 보여주면 될까요? 너무 밋밋합니다. 1분이 안되었다면 'n초 전', 1시간이 안되었다면 'n분 전', 1일이 안되었다면 'n시간 전'과 같이 알림이 생성된 후 시간이 얼마나 흘렀냐에 따라 UI를 다르게 보여줄 수도 있습니다.

또한, 모던 프론트엔드 개발에서는 '컴포넌트'라는 개념이 매우 중요시됩니다. 모든 화면은 컴포넌트가 조합되어 만들어집니다. 이 때 특정 상태에 변화가 일어난다면, 해당 변화는 그 상태의 영향을 받는 모든 컴포넌트에 필연적으로 '전파'되어야 합니다.

상태를 어디에 저장할 것인지, 상태에 변화를 어떻게 일으킬 것인지, 또는 변화를 어떻게 감지할 것인지, 그 변화를 어떻게 전파할 것인지, 변화/전파에 따라 상태를 어떻게 가공해서 UI에 보여줄 것인지 등, 이러한 '상태의 변화/전파를 처리하는 방법들'을 '상태 관리'라고 정의하고 싶습니다.

상태 관리는 왜 하는거고, 왜 중요할까?

위에서 언급했듯이, 프론트엔드에는 너무나 많은 상태가 존재하고, 이러한 상태들은 직/간접적으로 UI에 많은 영향을 주게됩니다.

따라서 상태를 제대로 관리하지 못하면 유저에게 어색한 경험을 제공하게 되거나, 서비스에 버그가 생기는 심각한 문제를 마주할 수 있습니다.

서버로부터 데이터를 불러와서 사용하는 상황들을 예시로 들어보겠습니다.

상태는 항상 프론트엔드 내부로부터 만들어지진 않습니다. 서버로부터 불러와 사용하는 데이터들은 외부의 상태라고 볼 수 있죠.

이 때, '팔로워 수' 데이터를 서버에서 불러오고, 그걸 사용하는 페이지가 여러개라고 가정해봅시다. 또한, '팔로워 수' 데이터는 서버 비용을 아끼기 위해 처음 불러왔을 때 전역 Cache Map에 저장해 둔다고 가정해봅시다.

검색을 하던 중, 검색 결과 페이지에서 아이유님이 눈에 띕니다. 검색 결과 페이지에서 아이유님의 팔로워 수는 1000명이라고 나옵니다.

아이유님이 너무 아름다우시고 노래도 너무 좋아서 프로필 페이지로 들어가 팔로우 버튼을 눌렀더니, 아이유님의 팔로워 수가 1 증가되어 1001명으로 보여집니다.

그런데 이 때, 검색 결과 페이지로 다시 돌아갔더니 아이유님의 팔로워 수가 그대로 1000명으로 보여집니다. 변화가 전파되지 않은 상황입니다.

위 예시에서 왜 이런 어색한 상황이 발생했을까요? 아무래도 개발자가 팔로우 버튼을 누르면 그냥 프로필 페이지 내부적으로 1만 더해서 보여주는 실수를 한 것 같습니다.

위 상황에서 개발자가 적절한 상태 관리를 했다면, 팔로우 버튼을 눌렀을 때 전역 Cache Map에서 '팔로우 수' 데이터를 함께 수정하거나, 서버로부터 '팔로우 수' 데이터를 refetch 해서 전역 Cache Map에 반영했을 것입니다.

이런 경우는 유저에게 어색한 경험으로 끝나겠지만, 상품을 찜하는 상황을 가정해봅시다. 상품은 상품 페이지와 상품 리스트에서 찜할 수 있다고 가정합니다.

유저하고 상품 페이지에서 상품을 찜하고 상품 리스트로 돌아왔더니, 상품이 찜한 상태가 아니어 보입니다. 그래서 유저는 찜 버튼을 또 눌렀습니다. 그랬더니 서버에서 이미 찜한 상품이라는 에러가 발생합니다.

이렇게 상태를 어떻게 관리하냐에 따라 유저 경험이 달라지고, 중단 없는 서비스 운영에 도움이 됩니다.

상태 관리는 어떻게 잘 하는가?

그렇다면 상태 관리를 더욱 효율적으로 잘할 수 있는 방법이 뭘까요?

저는 항상 크게 세 가지 원칙을 지키려고 노력합니다.

상태의 범위를 적절히 구분할 것

상태가 어떤 컴포넌트들 사이에 공유되냐에 따라, 상태가 어디에 위치할지를 잘 결정해야 합니다.

특정 컴포넌트 내부에서만 사용된다면 그 컴포넌트 내부의 상태로써만 가지고 있으면 될 것이고, 프론트엔드 코드 내에서 전역적으로 쓰이는 상태라면 전역 상태로써 가져가야 할 것입니다.

우선, 기본적으로 상태를 올바른 범위에 위치시키려 노력하면, 서로 의존하는 상태와 컴포넌트들의 물리적인 거리가 가까워지게 됩니다.

이런 경우, 변경사항이 일어났을 때 dead code가 생길 확률이 훨씬 적고, 혹여나 dead code가 생기더라도 리팩토링을 진행할 때 삭제하기가 수월합니다.

또한, 불필요하게 특정 상태를 남용(아주 적절한 예시로, react-router v5의 useLocation이 있습니다)하게 되어 퍼포먼스에 문제가 생기는 등, 특정 상태의 영향을 받지 않아도 되는 컴포넌트들에 대한 side effect도 사전에 방지할 수 있습니다.

최대한 필요한 곳에만 변화가 전파될 것

위에 '상태의 범위를 적절히 구분할 것'과 매우 밀접히 연결되는 원칙입니다.

React를 예시로 들면, React state는 변화가 일어났을 때 해당 state가 위치한 컴포넌트에 리렌더링을 일으켜 상태 변화를 전파합니다.

이 때, 상태를 잘못 위치시켜서 상태가 너무 상위의 컴포넌트에 위치해있거나, 하위 컴포넌트들이 React.memo 등으로 memoization이 안되있는 경우, 상태가 변화했을 때 수많은 하위 컴포넌트들이 전부 리렌더링되는 문제가 발생할 수 있습니다.

따라서 관련없는 컴포넌트에 영향이 가는 일은 최대한 지양해야합니다.

이 원칙을 지키기 위한 방법에는 상태에 따라 컴포넌트를 잘게 분리하는 방법, React Context를 사용해 Consumer들만 리렌더링을 일으키는 방법, RxJS를 사용해 Observable을 Subscribe한 곳에만 리렌더링을 일으키는 방법 등이 있습니다.

어떻게 저장하고 갱신할 것인지를 잘 설계할 것

위에서 상태 관리의 중요성을 설명할 때 적었던 예시에서도 드러나지만, 상태를 어떻게 저장하고 갱신할 것인지를 잘 설계하는 것은 매우 중요합니다. 단순 버그 방지를 넘어서 많은 비용을 아껴줍니다.

이번엔 상태 변화의 빈도(frequency)를 예시로 들어보겠습니다.

서버에서 불러오는 데이터들 중에는 변화가 매우 잦아서 캐시해서는 안되는 데이터와, 변화가 거의 없다시피해서 요청을 보내는게 아까운 수준의 데이터도 있습니다.

이 때 후자는 어떻게 관리하는게 좋을까요? 서버에 요청은 계속 보내지 않으면서 사이트가 꺼져도 상태를 유지할 수 있는 방법, local storage가 떠오르네요.

단, local storage같은 영구적인 storage는 일정 시간이 지났을 때나 특정 시점마다 삭제하는 로직이 필요할 것입니다.

실제로 저는 local storage를 활용한 client-side 캐시를 구현한 적이 있는데, API call도 많이 줄었고, 관련 컴포넌트의 렌더링 속도도 눈에 보이지 않을 만큼 엄청 빨라졌습니다.

변화가 거의 없는 데이터 말고, 변화가 실시간으로 보여져야하는 데이터의 경우 웹소켓을 사용해서 관리하는 방법도 있을 것입니다.

마무리

상태 관리를 잘 하는 것이 생각보다 많은 비용을 절약하게 해주는 것 같습니다. 그것이 서버 부하의 감소일 수도 있고, CS 인입의 감소일 수도 있겠죠.

이렇게 어떠한 개념에 대한 제 경험과 생각을 남기는 종류의 글은 처음 작성해보는 것 같아서 긴장이 되네요.

한편으론 뒤죽박죽이었던 생각이 차곡차곡 정리되는 기분이라 신기하기도 합니다.

이젠 어디가서 상태 관리에 대해 조금은 말해볼 수 있기를.. ㅠㅠ