상태
React에서는 컴포넌트 기반의 코드를 작성하게 됩니다.
컴포넌트는 여러 곳에서 재사용을 할 수 있고 추상화를 잘 이용하면 가독성이 좋은 코드를 작성하게 해줍니다.
컴포넌트는 상태를 가집니다.
예를 들면 이 블로그의 라이트/다크 모드, 카카오톡 대화창의 안읽은 메시지수 와 같은것들은 상태입니다.
상태는 사용자에게 보여지는 것을 의미하고 상태의 변경은 렌더링을 새로해주어야 함을 의미합니다.
사용자가 블로그의 라이트 모드에서 다크모드를 눌렀으면 어두운 화면을 보여주어야 하고
카카오톡 대화창의 메시지를 전부 읽었다면 안읽은 메시지 수는 0, 하나씩 올때마다 카운트를 올려주어야 할 것입니다.
컴포넌트는 자신이 정의한 상태를 가질수 있고, 서버로부터 데이터를 받아 자신의 상태로 정의할 수도 있고, 부모 컴포넌트로 부터 전달받아 상태를 가질수도 있습니다.
컴포넌트가 지니는 고유한 성질이나 사용자에게 보여줘야 하는 데이터가 상태라고 정의할 수 있을것 같고
상태는 리렌더링을 유발한다 정도로 정리할 수 있을것 같습니다.
이러한 상태를 효율적으로 관리하여 불필요한 렌더링을 방지하고 유지보수에 좋은 코드를 짜야합니다.
상태에는 크게
- 지역상태
- 전역상태
- Form의 상태
- 서버 상태
와 같은 것들이 있을수 있을것 같습니다.
좋은 상태 관리
상태는 제어되는 컴포넌트에 최대한 가깝게 위치하는 것이 좋다고 생각합니다.
둘의 거리가 멀면 멀수록 중간에 위치한 관련없는 컴포넌트들이 리렌더될수 있기 때문입니다.
리액트에서는 부모 컴포넌트의 상태가 변경되면 자식컴포넌트 또한 리렌더링 됩니다.
중간에 위치한 컴포넌트들은 불필요한 리렌더링을 겪을수 있습니다.
(리액트의 reconcilation 알고리즘에 의해 리렌더링이 결정됩니다.)
따라서 상태는 컴포넌트에 최대한 가깝게 위치되어 다른 컴포넌트들과의 결합도는 낮추고 상태와 사용하는 컴포넌트의 응집도는 높여야 할것 같습니다.
위에서 정의한 상태를 제어할때 아래와 같은 방식을 사용했었습니다.
지역상태는 커스텀 훅이나 리액트 컨텍스트를 사용하여 관리할 것 같습니다.
전역상태는 최대한 추가되지 않아야 한다고 생각합니다. 전역상태는 앱의 전체 렌더링을 수반하므로 주의해야 합니다.
전역상태의 관리에서는 recoil을 사용했었습니다. redux를 배워보려 했었으나 boiler plate코드가 비대하였고 학습량이 생각외로 많아져서 recoil을 접목했었습니다.
recoil을 사용할때는 hook의 사용과 비슷하였고 다른 서드파티 라이브러리와 다르게 recoil은 react를 만드는 facebook에서 만드는 first 파티 라이브러리 입니다. 직접 react의 생명주기에 접근하여 동작을 처리한다고 알고 있습니다.
그래서 더 react를 섬세하고 다룰수 있게 해주고 유지보수에도 유리한 측면을 가지고 있지 않을까 생각합니다.
Form의 상태는 Form 컴포넌트에서 상태를 정의하거나 커스텀 훅으로 제작하여 관리합니다.
input, errorMessage 컴포넌트를 이용할때는 각각의 컴포넌트들이 상태를 제어할 수 있도록 리액트 컨텍스트의 사용을 고려합니다.
리액트 컨텍스트를 사용하면 props drilling 없이 컴포넌트들 간에 상태를 공유할 수 있어 코드가 깔끔해집니다.
하지만 자칫 잘못하면 제어하는 상태와의 거리가 멀어질 수 있습니다. 코드 하나를 수정할때 여러 모듈을 탐험해야 할 수도 있습니다.
input, errrorMessage를 작성할때는 추상화를 이용할 수 있습니다. 추상화 단계를 올리고 의존성을 주입해 주어서 선언형으로 코드를 작성한다면 의미가 잘 드러나고 이해하기 좋은 코드를 작성할 수 있습니다.
이때 의존성을 주입해주지 않거나 잘못 주입한다면 자칫 갑자기 동떨어진 코드가 왜 등장하는지와 같이 느낄수 있을것 같습니다. 그래서 의도적으로 의존성을 드러내는 것이 좋을것 같습니다.
Form의 상태를 제어하기 위한 좋은 라이브러리 들이 이미 존재합니다.
formik, react-hook-form과 같은 라이브러리 들이 있습니다.
Formik은 제어컴포넌트를 기반으로 구현되어있고 react-hook-form은 비제어 컴포넌트를 기반으로 구현되어있습니다.
validation 라이브러리인 yup과 결합하여 사용한다면 더 강력한 코드를 작성할 수 있습니다.
서버의 상태를 관리하는 것은 매우 중요합니다.
서버의 상태 관리를 위해 react-query 라이브러리를 사용합니다.
swr또한 자주 사용되는 라이브러리로 알고있습니다.
사용하는 이유에 대해서 아래에 천천히 풀어보겠습니다.
react에서는 서버에 요청을 보내고 서버에서는 db를 조회한후 데이터를 입맛에 맞게 조합하여 front로 보내줍니다.
cpu의 연산에 비해 네트워크 요청은 시간이 오래걸립니다. 이러한 상황에서 부하가 걸리는 것을 I/o bound라 합니다.
그래서 캐시를 효율적으로 이용해야 합니다.
동일한 요청을 서버에 보내지 않으면 그만큼 시간을 아낄수 있습니다.
네트워크에서는 캐시를 이용하기 위하여 header에 cache-control 속성을 다뤄야 합니다.
cache-control에는 max-age, no-store, no-cache, must-revalidate, stale-while-revalidate와 같은 값들이 들어갈 수 있습니다.
위 헤더를 사용하면 메모리에서 요청에 대한 응답을 관리하고 동일한 요청시 서버로 요청을 보내지 않고 메모리에서 꺼내어 응답해줍니다.
이러한 서버 캐싱을 다양한 방법으로 이용할 수 있습니다.
session storage, local stoarge와 같이 브라우저의 storage에 저장하여 사용할 수 도 있고
전역상태로 관리를 할 수도 있습니다. 상태로 관리를 위해 redux를 도입할 수 있었습니다.
redux는 기존의 mvc 패턴에서 점점 관리되지 않던 상태관리를 일관된 흐름을 이용한 flux 패턴을 기반으로 탄생한 상태관리 라이브러리 입니다. 상태가 존재하는 store가 존재하고 컴포넌트에서는 dispatch를 통해 action을 호출하면 store에서 상태가 변하고 변경된 상태를 사용하는 컴포넌트는 리렌더링 됩니다.
전역상태의 관리를 위하여는 좋은 툴이지만 서버에 요청을 보내는 비동기 동작을 하기 위해서는 redux-thunk, redux-saga와 같은 미들웨어 라이브러리를 추가해야 했습니다.
저는 redux-saga를 사용해보면서 두개의 의문점이 생겼었습니다.
1. 서버의 상태를 컴포넌트들의 전역 상태와 같이 관리하는게 맞는건가? -> saga의 분리로 절충안을 내놓을 순 있을것 같습니다.
2. 서버의 상태가 프론트 코드에 위치하고 있는게 맞는걸까?
2번은 아직 좀 더 생각중입니다.
제가 정확히 가지고 있는 의문이 저 wording으로 표현될 수 있는지는 좀 더 생각을 해봐야 할것 같습니다.
두서없이 생각을 적어보자면
상태의 변경은 리렌더링을 유발하고 전역 상태의 변경은 전체의 리렌더링을 수반한다고 하였습니다.
즉 전체에 영향을 끼치는 상태가 전역상태로 존재해야 함을 의미한다고 생각합니다.
서버의 상태가 전체에 영향을 끼치는 전역상태로 관리되는게 맞는걸까 생각이 들었습니다.
위에서 정의한 전역상태의 정의와 맞지 않다고 느껴졌습니다.
서버의 캐싱을 위해 전역상태를 이용한 것 같았습니다.
상품을 보여주는 페이지에서 비동기로 api를 호출하여 가져온 정보를 store에서 업데이트 하면 전역에서 관리되는 상태는 변경되지만 전체 앱은 리렌더링 될 필요가 없습니다.
상품을 보여주는 페이지만 리렌더링이 되면 됩니다.
또한 redux-saga를 이용하여 비동기 통신을 이용할 수는 있지만 boiiler plate코드가 무수히 늘어갔습니다.
store가 너무 비대해졌고 캐싱과 같은 기능을 구현하기 위해서 필요한 Interface를 생각해보았지만 기능이 너무 복잡해질것 같아 실제로는 구현하지 못했습니다.
필요한 interface에는 key값, 요청한 시간, 유효 시간, 데이터의 상태 등과 같은것들이 있지 않을까 생각했었습니다.
그래서 react-query를 적용하였습니다.
react-query는 공식문서에 'Fetch, cache and Update data in your React and React Native applciations all without touching any "global state"' 라 명시되어있습니다.
전역상태에 영향을 끼치 않으면서 데이터를 fetch, cache, update해준다는 의미가 됩니다.
리액트 공식문서에는 서버 상태를 아래와 같이 정의하고 있습니다
- 컴포넌트에서 제어하거나 소유하지 않는 원격의 위치에 존재하여야 한다
- fetching과 updating을 위한 비동기 API가 필요하다
- 공유되는 정보이고 다른사람들로 인하여 변경될 수 있습니다.
- 어플리케이션에서 신경쓰지 않는다면 out of date 가 될수도 있습니다
react-query에서는 기본 cache 시간이 300초로 정해져 있고 stale time은 0으로 정해져 있습니다.
stale의 의미는 fresh와 비슷한 뜻입니다.
신선하지 않기 때문에 stale time이 지나면 데이터를 요청하겠죠.
이때 cache time내에서는 서버에 요청을 보내지 않고 캐시에 있는 데이터를 return 해줄겁니다.
또한 서버상태를 key값으로 관리를 할 수 있습니다.
key값에는 배열 혹은 string과 같은 값들이 들어갈 수 있습니다.
프론트 코드상에서는 캐시의 key값만 정의해두고 cache, stale 시간등을 조절해서 사용하면 컴포넌트의 전역상태와 별개로 관리할 수 있어 편리해졌었습니다.
http의 cache-control 특성에 stale-while-revalidate 옵션이 있습니다. stale time을 정의할 수 있는 속성입니다.
max-age는 30, stale-while-revalidate는 30이라고 가정해보겠습니다.
30초 이내에 요청은 cache의 결과를 보여주고
31 ~ 60초에 요청이 들어오면 우선 cache에 있는 결과를 보여주고 신선하지 않은 데이터 이기 때문에 서버에 요청을 보냅니다. 응답을 받은후에는 값을 교체해줍니다.
그러면 cache에 있는 결과는 신선한 상태가 됩니다.
이러한 로직을 react-query에서 관리해주니 많은 장점이 있다고 판단하였습니다.
(아직 배움이 많이 필요한 주니어의 생각입니다. 잘못된 부분들은 따끔하게 알려주시면 큰 도움이 됩니다!)
'Frontend > React' 카테고리의 다른 글
React 상태관리의 과거, 현재 그리고 미래 (번역글) (0) | 2022.04.09 |
---|---|
Recoil 🌱 (0) | 2022.04.08 |
React css-in-js (0) | 2022.04.06 |
React form 블로그 따라 다루기 (0) | 2022.04.06 |
React keys (0) | 2022.04.02 |