포스트 카드를 클릭했을 때 조회수를 증가시키고 새 창에서 포스트를 여는 기능을 구현하면서 마주친 문제들과 해결 과정을 공유하기 위해 글을 작성하게 되었다.
처음에는 사용자가 포스트 카드를 클릭하면 새 창에서 해당 포스트를 열고, 동시에 조회수 증가 API를 호출하도록 했다. 클릭 이벤트 핸들러에서 window.open()
메서드로 창을 열고, React Query의 refetch를 사용해 API를 호출했다.
const handleClick = () => {
if (!post.path) return;
window.open(post.path, "_blank", "noopener,noreferrer");
refetch().catch((error) => {
console.error("조회수 증가 실패", error);
})
}
단순하게 구현할 수 있었지만, 작업의 성공과 실패 처리가 불완전했다. window.open()
이 실패할 경우에 대한 처리가 없었고, API 호출 실패 시에는 단순히 콘솔에 에러를 출력하는 것이 전부였다. handleClick()
함수가 단일 책임 원칙을 지키고 있다고 보기도 어려울 것이다. 그리고 클릭 이벤트에 분석 로직을 추가한다거나, 조회수 증가 전후로 특정 작업을 또 수행해야 할 경우 함수가 더 복잡해질 것이라고 느껴 함수형 프로그래밍을 도입해보았다.
코드의 유지보수성과 확장성을 높이기 위해 함수형 프로그래밍의 Pipe 패턴을 도입했다. Pipe 패턴은 여러 함수를 순차적으로 실행하면서 데이터를 변환하는 방식이다. 각 함수는 독립적인 단일 책임을 가지며, 이전 함수의 출력이 다음 함수의 입력으로 전달된다.
구현을 위해 먼저 타입과 상태 인터페이스를 정의했다.
interface PostWithState {
post: Post;
isWindowOpened: boolean;
}
여기서 isWindowOpened는 창이 성공적으로 열렸는지를 추적하는 상태값이다. 인터페이스를 기반으로 두 개의 함수를 구현했다.
const openPost = ({ post }: Pick<PostWithState, "post">): PostWithState => ({
post,
isWindowOpened: window.open(post.path, "_blank", "noopener,noreferrer") !== null,
});
const incrementView = ({ post, isWindowOpened }: PostWithState): PostWithState => {
if (isWindowOpened) {
refetch().catch((error) => {
console.error("조회수 증가 실패: ", error);
});
}
return { post, isWindowOpened };
};
openPost()
는 새 창을 열고 그 결과를 상태로 관리한다. increment()
는 창이 성공적으로 열렸을 때만 조회수 증가 API를 호출한다. 이 두 함수를 pipe로 연결했다.