
과거 React와 TypeScript를 사용해서 화면을 만들고 API를 연동하는 프로젝트에 참여한 적이 있습니다.
당시에는 프론트엔드 경험이 많지 않았기 때문에 맡은 기능을 구현하기 위해 강의도 따로 듣고, 필요한 내용을 찾아가며 프로젝트를 진행했었는데, 일정 안에 기능을 구현하는 데 집중하다 보니 코드는 정상적으로 동작했지만, 시간이 지나고 다시 보니 정확한 원리를 이해하지 못한 채 사용했던 부분도 있었습니다.
이번에는 프로젝트 당시 헷갈렸던 내용을 중심으로 다시 정리해 보겠습니다!
useEffect와 의존성 배열
프로젝트에서 가장 많이 사용했던 코드 중 하나가 화면에 진입했을 때 API를 호출하는 코드였습니다.
useEffect(() => {
fetchData();
}, []);
당시에는 useEffect를 화면이 열릴 때 실행하는 함수 정도로만 생각했었습니다.
하지만 다시 공부하면서 확인해 보니 useEffect는 단순히 API를 호출하기 위한 기능이 아니라, 컴포넌트 외부의 상태나 시스템과 값을 동기화하기 위해 사용하는 기능에 가까웠습니다.
특히 의존성 배열에 어떤 값을 넣어야 하는지가 많이 헷갈렸습니다...
const searchCondition = {
keyword,
status,
};
useEffect(() => {
fetchData(searchCondition);
}, [searchCondition]);
위 코드처럼 객체를 그대로 의존성에 넣으면 렌더링할 때마다 새로운 객체가 만들어질 수 있고, 의도하지 않게 useEffect가 반복 실행될 수 있었습니다.
이 경우에는 객체 전체를 의존성으로 넣기보다 실제로 변경을 감지해야 하는 값을 명확하게 지정하는 것이 더 자연스러웠습니다.
useEffect(() => {
fetchData({ keyword, status });
}, [keyword, status]);
예전에는 의존성 관련 경고가 나오면 일단 값을 추가하는 방식으로 처리했는데,
이제는 해당 effect가 정확히 어떤 값의 변경에 반응해야 하는지를 먼저 생각해야 한다는 기준이 생겼습니다.
API 응답 타입과 화면 타입
TypeScript를 처음 사용할 때는 백엔드에서 내려주는 API 응답 구조를
그대로 interface로 만들고 화면에서도 동일하게 사용했습니다.
interface UserResponse {
userId: number;
userName: string;
statusCode: string;
createdAt: string;
}
이 방식도 동작에는 문제가 없지만, API에서 사용하는 데이터와 화면에서 사용하는 데이터는 목적이 다를 수 있습니다.
예를 들어 API에서는 상태값을 코드로 내려주지만, 화면에서는 사용자에게 한글 상태명으로 보여줘야 할 수 있어야 합니다.
interface UserViewModel {
id: number;
name: string;
statusLabel: string;
createdDate: string;
}
이럴 때는 API 응답을 화면에서 바로 사용하기보다 중간에 변환 과정을 두는 것이 더 관리하기 편했습니다.
const toUserViewModel = (data: UserResponse): UserViewModel => ({
id: data.userId,
name: data.userName,
statusLabel: data.statusCode === "A" ? "활성" : "비활성",
createdDate: data.createdAt.substring(0, 10),
});
예전에는 타입을 하나 더 만드는 것이 오히려 복잡하다고 생각했지만, 다시 보니 API가 변경되더라도 화면 전체를 수정하지 않고 변환 부분만 수정할 수 있다는 장점이 있었습니다.
백엔드와 데이터베이스 업무를 경험한 뒤 다시 보니, 프론트엔드에서도 데이터가 넘어오는 경계를 분리하는 것이 중요하다는 점이 더 잘 이해됐습니다!
State 관리
당시에는 화면에서 사용하는 값이라면 대부분 useState로 관리해야 한다고 생각했습니다.
const [users, setUsers] = useState<User[]>([]);
const [activeUsers, setActiveUsers] = useState<User[]>([]);
useEffect(() => {
setActiveUsers(users.filter((user) => user.active));
}, [users]);
하지만 다시 살펴보니 activeUsers는 이미 users를 기준으로 계산할 수 있는 값이었습니다.
const activeUsers = users.filter((user) => user.active);
이처럼 기존 값으로 계산할 수 있는 데이터를 별도의 state로 관리하면, 원본 데이터가 변경될 때마다 함께 맞춰줘야 했습니다.
관리해야 할 값이 늘어나면서 두 데이터가 서로 달라질 가능성도 있었습니다.
또한 API를 통해 가져온 데이터와 검색어, 체크박스, 모달 상태처럼 화면에서만 사용하는 값은 성격이 다르다는 점도 다시 정리할 수 있었습니다.
예전에는 이런 값들을 모두 하나의 컴포넌트 안에서 useState로 관리했었지만 데이터가 많아질수록 로딩 처리, API 재호출, 사용자 입력 상태가 한곳에 섞이면서 코드를 확인하기가 어려웠습니다.
앞으로는 state를 추가하기 전에 반드시 저장해야 하는 값인지, 기존 데이터를 활용해 계산할 수 있는 값인지 먼저 확인해야되는 걸 알았습니다!
마무리
프로젝트 당시에는 부족한 부분을 빠르게 공부하고, 맡은 기능을 일정 안에 완성하는 데 집중했습니다.
시간이 지난 뒤 당시 작성했던 코드를 다시 살펴보니 사용 방법만 알고 넘어갔던 부분과 정확한 원리를 이해하지 못했던 부분이 함께 보이게 됬습니다.
특히 useEffect의 의존성 배열, API 응답 타입과 화면 타입의 분리, state 관리 기준, 상태 변경 시점은 당시 자주 헷갈렸던 내용이였는데, 공부하고 보니 그 때 당시에 헷갈렸던 것들을 더 제대로 이해를 할 수 있어서 오래 가져가게 되었어요
이번 리와인드를 통해 단순히 동작하는 코드를 작성하는 것보다, 나중에 다시 확인해도 이해하기 쉽고 수정하기 편한 구조로 작성하는 것이 중요하다는 점을 다시 느낄 수 있었습니다!
'개발지식' 카테고리의 다른 글
| 프로토콜(Protocol)이란 무엇인가? (0) | 2024.03.02 |
|---|