Recoil 상태관리 라이브러리
기업 프로젝트 진행 중, 이전 분이 작성하신 코드를 보니 상태관리 라이브러리로 Recoil을 사용하셔서 관련하여 정리를 해본다!!
리코일은 상태관리 라이브러리로, 애플리케이션의 상태를 효과적으로 관리해준다.Atom
과 Selector
라는 개념을 활용해 상태를 정의하고 파생된 데이터를 생성한다.
Atom
- recoil에서 상태의 최소 단위.
- key: atom의 이름으로 유일해야 한다.
- default: 상태의 기본 값. '[]' 이면 'useState([])' 이라고 생각하면 된다.
// 로그인 사용자 이름
export const userNameState = atom({
key: "UserNameState",
default: "",
});
// 로그인 사용자 역할.
export const userRoleState = atom({
key: "UserRoleState",
default: "",
});
코드처럼 key, default 값으로 전역 상태값을 할 수 있다.
Selector
- Atom을 기반으로 파생된 데이터를 생성하는 역할을 한다.
- 순수 함수로 작성되어야 하며, 동일한 입력값이 주어지면 항상 동일한 값을 출력한다.
- 즉 부작용(side effect)이 없는 순수한 함수,
- Atom을 유지하면서 파생된 데이터를 가져올 수 있어, 상태 간 관계를 표현하고 유지보수하기 용이하다.
const derivedSelector = selector({
key: '키값',
get: ({ get }) => {
const originalValue = get(atom);
// 원본 값을 변형하여 반환
return transformedValue;
},
});
여기서 get은 selector가 읽기에 의존하는 atom 값을 가져오기 위해 사용된다.
get함수를 사용해 atom값을 가져올 수 있다. 이외에 다양한 유틸리티 함수가 제공된다.
import { selector } from 'recoil';
import { userNameState, userRoleState } from './atoms';
export const formattedUserInfoSelector = selector({
key: 'FormattedUserInfoSelector',
get: ({ get }) => {
const userName = get(userNameState);
const userRole = get(userRoleState);
// 이름과 역할을 조합하여 특정 형식으로 표시
const formattedUserInfo = `${userName} (${userRole})`;
return formattedUserInfo;
},
});
예제 코드로 Atom을 기반으로
위 코드에서 formattedUserInfoSelector는 userNameState과 userRoleState Atom을 기반으로 파생된 데이터를 생성하는 Selector입니다. get 함수를 통해 해당 Atom의 값을 가져와 사용자의 이름과 역할을 변수에 할당합니다. 그리고 이름과 역할을 조합하여 특정 형식으로 표시한 후, formattedUserInfo 변수에 저장합니다.
React Strict Mode
리액트에서 기본적으로 개발 환경에서 렌더링이나 이런저런 이슈들을 미리 파악하기 위해 컴포넌트들을 두 번씩 렌더링하는 Strict Mode를 활성화시킨다. 이 과정에서 같은 훅도 여러번 실행된다. 이를 잠시 비활성화 시키면 빌드 후 프로덕션 환경처럼 실제 렌더링되는 횟수를 확인해볼 수 있다.
기업프로젝트에서 이전 기수 분의 코드를 이어받으면서 요구사항 해결와중에 간간히 리팩토링하고싶은게 보이는데
그 중에서도 코드가 한 파일에 뭉텅이로 있어서 리렌더링이 일어날 이유가 없는 컴포넌트들도 함께 된다는 점이다.
이에 컴포넌트를 분리하는 과정에서 리액트 개발자도구와 network를 확인하면서 렌더링이 필요없는 컴포넌트들의 렌더 여부를 확인해보았고, 그 과정에서 Strict Mode가 요런거구나~ 하고 알아보았다.
공식문서 왈
Strict Mode enables the following development-only behaviors
- 퓨어하지않은 렌더링으로인해 생기는 버그를 찾기 위해 리렌더한다
- 놓친 Effect cleanup을 찾기 위해 Effects를 rerun한다.
- deprecated된 API들을 체크한다.
즉 StrictMode로 감싸진 컴포넌트는 나름 보장된 녀석들이라는 것?
페이지에서 검색필터 컴포넌트 분리
Page 컴포넌트에서는 모든게 싹다 들어있는데,,
검색필터를 설정한 후, 검색을 누르면 이를 토대로 params를 생성해 데이터를 부르는 형식이다.
데이터는 페이지네이션으로 보여지는데 pIndex를 통해 페이지네이션을 이동할 때마다 똑같은 검색옵션에 pIndex만 바뀐채로 요청을 보낸다.
이게 좀 비효율적이라고 생각이 들어 첫 번재로 페이지에서 검색필터 컴포넌트를 분리시켜주었다.
검색필터 컴포넌트에서는 오로지 선택된 옵션을 토대로 쿼리에 들어갈 파라미터만 반환해주는 기능을 수행하도록 했다.
이를 위해 검색컴포넌트를 담고있는 Page 컴포넌트에서 queryParams
를 state로 관리하였고, setQueryParams
를 검색필터컴포넌트로 넘겨주어 쿼리파라미터 상태변경을 할 수 있도록 해주었다.
그리고 Page 컴포넌트에서는 이를 queryParmas에 pIndex 쿼리를 붙여서 데이터 요청을 보내도록 하였다.
...
const [queryParams, setQueryParams] = useState(
`pStatus=OPEN&dStartDate=${formatDateTime(
startDate,
startTime,
)}&dEndDate=${formatDateTime(endDate, endTime)}`,
);
...
const fetchTicket = async () => {
try {
// TicketFilter에서는 검색을 눌렀을 때 params만 설정해서 넘겨주고, 페이지네이션마다
// pIndex를 params에 붙여보내도록 요청을 보냈습니다.
// todo: 데이터를 조금씩 불러오는 방식은 없을까?
const params = new URLSearchParams(queryParams);
params.append("pIndex", currentPage);
const fetchedData = await getTickets(params.toString());
if (fetchedData.tickets.length === 0) {
setFailureMessage("조회된 데이터가 없습니다.");
setShowRequestFailureModal(true);
setTotalPages(1);
setSelectedTickets([]);
} else {
setFilterConditions(params);
setTickets(fetchedData.tickets);
setTotalPages(fetchedData.totalPage);
setTotalTickets(fetchedData.total);
setSelectedTickets([]);
}
} catch (error) {
console.error("Error fetching tickets:", error);
setFailureMessage("요청 처리에 실패하였습니다.");
setShowRequestFailureModal(true);
}
};
여기서 마주했던 에러 두 가지.
- queryParams state의 초깃값을 설정해주지 않고 검색필터 컴포넌트에서만 useEffect로 초기렌더링 시, setQueryParams를 통해 초깃값을 넣어주도록 코드를 작성했다. => 아주아주 잘못된 생각. 부모컴포넌트인 Page 컴포넌트가 렌더되고 데이터를 요청하는 시점엔 검색필터컴포넌트의 초깃값이 아니라 Page 컴포넌트의 초기값이 들어가기 때문에 요청이 잘못되게 된다.
params.append("pIndex", currentPage);
처음에는const params= queryParams
이렇게 복사해서 쓸 수 있다 생각했는데, 이렇게 한채로 .append를 하면 queryParams도 함께 변한다. 이는 js에서 객체를 다른 변수에 할당하면 참조가 전달되기 때문이다. 따라서 아예new URLSearchParams
로 새로운 객체를 선언해서 써야했다.
멘토님께서 이 코드를 보시고 구조적인 개선을 조금 더 주면 좋을 것 같다하셨는데, 우선은
queryParams의 초기값을 null로 두고 데이터 fetch 호출 전에 null check를 하는 방식으로 최초 api 호출을 피할 수 있을 것 같다하셔서 적용해보았다.
이제 Page.tsx가 렌더되면-> useEffect 호출-> queryParams가 null이므로 fetch호출안함 -> 검색필터 컴포넌트에서
useEffect로 인해 queryParams 변경 -> Page컴포넌트에서 useEffect로인해 데이터 패치!
이렇게 첫 렌더시 한번만 Api 콜을하게 바꿀 수 있었다.
ReactDevTool
리액트사용하면 꼭 써야할 리액트개발자도구
여기서 hook을 통해 리렌더링된 경우 어떤 훅을 통해 리렌더링을 알려주는 옵션을 켤 수 있다.
바로 'Highlight Updates'
개발자도구-> 리액트 탭 -> 설정아이콘 -> components ->Always parse hook names from source 켜기
'TIL' 카테고리의 다른 글
리액트 다루는 기술 1주차 스터디 정리 (0) | 2024.01.16 |
---|---|
[기업플젝 기록] 리액트 - useContext로 모달 관리하기 (0) | 2023.12.14 |
11/8 next.js 프로젝트 진행 - radix progress bar (0) | 2023.11.08 |
SQL 고득점 Kit 풀이- SELCT, STRING& DATE (1) (0) | 2023.10.20 |