본문 바로가기

2024-08-22 컴포넌트 재사용에 따른 조건부 로직 구현 방법

codeConnection 2024. 8. 22.

'팀 프로젝트'란 무엇인가

부트캠프에서 마지막 프로젝트를 진행하면서 엄청난 깨달음을 얻었다.

 

이번 프로젝트까지 팀 프로젝트를 5~6번은 진행한 것 같은데,

마지막 프로젝트는 약 한 달 간 진행하는 나름(?) 대규모 프로젝트였고,

이전 프로젝트는 1~2주 내외의 비교적 간단한 프로젝트였다.

 

앞에 짧게 진행했던 프로젝트들을 거치면서 많이 성장했다고 느꼈었는데

'팀 프로젝트'라는 것에 대한 접근법 자체가 틀렸었던 것 같다.

 

지금 와서 생각해보니 그 전의 프로젝트는

4~5명의 개인 개발자들이 공용 리포지토리를 하나 만들어서

각자 맡은 페이지나 기능을 만들어서 합치는 과정에 불과했던 것이었다는 생각이다.

 

내가 이런 생각을 하게 된 이유는 최종 프로젝트가 힘들어도 너무 힘들다는 것에서 '왜 그럴까'라는 고민에서 시작되었다.

 

먼저 아래가 우리의 와이어프레임이다.

 

와이어프레임이 제작되고 큰 착각을 했다.

 

한 달이라는 제작 기간 치고 그다지 많지 않다는 생각이었다.

페이지 수만 보면 일주일 동안 했던 프로젝트와 크게 다를 게 없었기 때문이었다.

 

그런데 나는 최종 프로젝트를 시작하면서부터 팀원들에게 한 가지 걱정을 말했었다.

 

이 바로 직전 팀 프로젝트에서 배포한 사이트가 느려도 너무 느렸다는 것이다. 도저히 서비스 할 수 없을 정도로 말이다.

 

그래서 프로젝트 아키텍처를 제대로 구성하고, 모듈을 재사용 가능하도록 구현하여

메모리 낭비를 최소화고 성능 최적화를 꾀하자고 이야기했다.

 

내가 내 입으로 이야기했으니 당연히 나부터 신경 써야 하지 않겠는가.

 

 

위 사진을 보면 사용자의 프로필을 보여주는 이 컴포넌트가 크게 네 개의 패턴으로 재사용 할 수 있을 거라는

예상이 가능하지 않겠는가?

 

이 와이어프레임을 개발자들이 제작했다면 처음부터 재사용 가능한 컴포넌트들을 명시하고 분류했겠지만,

우리 서비스의 와이어프레임은 전문 UI 디자이너가 제작한 와이어프레임이기 때문에

(개발적으로) 재사용 가능한 컴포넌트를 처음부터 설계하는 데는 어려움이 있었다.

 

어떤 컴포넌트가 어디에서 재사용되는 지는 물어봐서 알 수도 있겠지만,

구현하는 개발자가 직접 보는 게 더 빨랐고,

또 컴포넌트들 간에는 어떤 차이가 있는지 파악하는 것은 디자이너에게 물어봐도 모른다.

개발자만 할 수 있는 것이기 때문에 미묘한 차이까지 파악해야 한다.

 

개발을 시작하기 전에 이것을 파악하는데 꼬박 2주가 걸렸다.

단순히 이 컴포넌트는 여기에서도 저기에서도 쓰이는 구나! 가 아니라, 알고리즘에서 어떤 차이를 보이고 어떻게 구현해야 하는지까지 생각하는데 2주가 걸렸다는 의미이다.

 

위 사진 3장은 전부 다른 페이지다.

첫번째는 게시글에서 보여주는 유저 프로필 카드이고,

두번째는 마이페이지이고, 세번째는 다른 유저의 프로필 페이지다.

 

게시글에서는 또 이렇게 나뉜다.

  • 로그인을 했는가?
    • yes : 정상적으로 보여준다
    • no : 블러처리된 오버레이 모달을 띄워 로그인을 유도한다
  • 내 게시글인가?
    • yes : 아무런 버튼도 보여주지 않는다
    • no : 팔로우 하기 버튼을 보여준다
  • 이미 팔로우 되어 있는가?
    • yes : 팔로우 취소 버튼을 보여준다
    • no : 팔로우 하기 버튼을 보여준다

 

마이페이지는 이렇게 나뉜다.

  • 내가 내 프로필에 들어왔는가?
    • yes : 팔로우 하기 버튼이 아니라 프로필 수정 페이지를 보여준다
    • no : 팔로우 하기, 팔로우 취소 버튼을 보여준다
      • 그런데 게시글에서의 팔로우 버튼과는 다르게 하단에 팔로잉과 팔로워 수가 렌더링 되고 있는데 이것을 invalidate 시킨다.

 

본인이 제작한 페이지, 컴포넌트가 이것 뿐이 아니니 이런 식으로 반복되는 로직이 십 수 개는 되다 보니 참으로 오래 걸렸다.

 

의도하고 업무 분배를 한 것은 아닌데, 본인이 맡은 파트가 나 혼자만 작성해서 되는 건 회원정보 수정 페이지 빼고는 하나도 없다. 전부 재사용이 된다. 그리고 그 재사용은 내가 하는 게 아니라 다른 팀원이 해야 한다. 따라서 다른 팀원이 어렵지 않게 사용할 수 있도록 코드를 직관적으로 짜야 하고, 필요하지 않는 기능은 옵셔널한 props로 받을 수 있도록 계속해서 리팩토링 해야 했다.

 

구현하고자 하는 것

서론이 길었지만 그래서 구현하고자 하는 것이 무엇이냐?

내가 만든 컴포넌트가 어디에서 사용되는지 어떻게 가려낼 것인가이다.

위 사진 예시처럼, 게시글 상세페이지에서 컴포넌트가 호출된 것인지, 마이페이지에서 호출된 것인지에 따라 다른 요소를 렌더링해야 하기 때문이다.

 

내가 생각한 가장 간단한 방법은 url path에서 호출된 페이지를 식별할 수 있는 세그먼트를 추출하여 구분하는 방법이다.

이를 위해 먼저 window 객체에 접근하는 방법을 사용했다.

 

useEffect(() => {
        const getCurrentBuddyId = buddy?.buddy_id;
        setFollowerId(getCurrentBuddyId);

        const fetchTripMasterId = async () => {
            try {
                const urlTripId = window.location.pathname.split('/').pop();
                const tripMasterIdResponse = await fetch(
                    `/api/contract/trip/masterId?trip_id=${urlTripId}`,
                    {
                        method: 'GET',
                    },
                );
                
                //... 이하 생략

 

const urlTripId = window.location.pathname.split('/').pop();

이것을 처음에는 useEffect 훅 외부에서, 코드 최상단에서 작성했다.

url을 최대한 빨리 불러와야 이후의 로직을 결정할 수 있지 않을까라는 생각 때문이었다.

 

그런데 웬걸? 됐다 안 됐다 한다.

되면 되는 거고 안 되면 안 되는 거지 됐다 안 됐다 하는 것은 뭐람?

우리가 디버깅을 할 때 어떤 패턴에서 에러가 발생하는지 파악을 하기 마련인데, 이런 불특정한 상황에서는 도무지 뭐가 문제인지 파악하기가 힘들다.

 

이유를 알아냈다.

window 객체는 브라우저에 의존하기 때문에 서버사이드 렌더링에서는 window 객체를 참조하면 호환성 이슈가 발생할 수 있다는 것이다.

 

그나마 안정적으로 처리하는 방법은,

본인처럼 useEffect 훅 내부에서 실행하여 브라우저에 DOM 요소가 모두 생성되고 나서 코드가 실행되게 하는 것이다.

이렇게 하면 되긴 된다. 하지만 불완전한 방법이고 대체 방법이 없는 것도 아니기 때문에 개선이 필요하다.

 

구현방법

그래서 최종적으로 선택한 방법은 React 훅을 사용하는 방법이다. 두 가지 옵션이 존재한다.

useRouter 훅을 사용한다 vs usePathname 훅을 사용한다.

 

여러 차이가 있지만 가장 큰 차이는,

서버사이드 렌더링에서 사용할 수 있느냐 아니냐일 것이다.

useRouter는 서버 사이드 렌더링과 클라이언트 사이드 렌더링에서의 사용 둘 다 지원하지만 useRouter 훅이 url에서 경로를 파악하는 기능만 제공하는 것이 아니라 router.back, router.push, router.refresh 등 많은 기능을 지원하기 때문에

훅 자체가 무겁다. 여러 기능 때문에 불필요한 오버헤드가 발생할 수 있다.

 

저런 기능들을 다 사용할 것이 아닌데 단순이 현재 경로만 얻기 위해서 사용하는 거라면 최적화의 부담이 따른다.

물론 서버 사이드 렌더링에서 구현하려면 어쩔 수 없이 useRouter를 사용해야 한다.

 

그리고 usePathname 훅은 현재 경로를 파악하는 기능만을 제공한다. 그래서 useRouter보다 빠르다.

큰 단점은 클라이언트 사이드 렌더링에서만 사용 가능하다는 것이다.

 

하지만 본인이 이 기능을 사용하는 컴포넌트는 클라이언트 컴포넌트이고,

현재 경로를 얻어오는 기능만 필요하기 때문에 최적화를 위해 usePathname 훅을 사용하기로 결정했다.

usePathname 훅으로 현재 경로 추출하기

훅 import 하기

import { usePathname } from 'next/navigation';

훅 호출하기

const pathname = usePathname();

특정 경로가 맞는지 관리할 상태 만들기

const [hasRank, setHasRank] = useState<boolean>(false);

 

useEffect 훅 내부에서 현재 경로 추출하기

useEffect(() => {
        // URL 경로에서 rank가 있는지 확인
        setHasRank(pathname.includes('rank'));
    }, [pathname]);

useEffect 훅으로 관리하는 이유는 아래와 같다.

  • 클라이언트 컴포넌트이기 때문에 이것 말고도 다른 상태들이 많은데, 그 상태들이 업데이트 될 때마다 이 상태 또한 리렌더링 되므로 불필요한 메모리 누수가 발생한다. 어차피 페이지가 바뀌지 않는 한 pathname이 바뀔 일은 없기 때문에 다른 상태가 업데이트 되었다고 리렌더링 할 필요가 없다.
  • 그러나 pathname이 어떠한 이유로 바뀌게 되면 그 때만 재실행하면 되기 때문에 pathname을 의존성 배열에 넣어 주어 컴포넌트가 처음 마운트 되었을 때, pathname의 변화가 생겼을 때에만 실행되도록 생애 주기를 관리해주는 것이다.

그렇게 rank라는 pathname이 있다면 위에서 만든 상태를 업데이트 한다.

상태에 따라 조건부 렌더링 하기 또는 조건부 로직 구현하기

return (
        <>
            {hasRank && !isOwnCard && (
                <button
                    className="absolute top-0 right-0 text-xl mr-1 mt-1"
                    onClick={handleFollowToggle}
                    disabled={isButtonDisabled}
                >
                    <span className={isFollowing ? 'text-main-color' : ''}>
                        {isFollowing ? '♥' : '♡'}
                    </span>
                </button>
            )}
        </>
    );

hasRank의 불리언 값에 따라 하트 버튼이 표시될 수도, 표시되지 않을 수도 있는 조건부 렌더링을 구현했다.

 

useRouter 훅으로 경로 추출하기

자꾸 안 되길래 찾아보니 스택오버플로우에서 App router 방식에서는 useRouter 자체로는 현재 경로를 불러올 수 없다고 한다.

app router 방식에서는 usePathname을 사용하는 것이 맞다고 한다.

다만 위에서 이야기했듯 클라이언트 컴포넌트에서만 동작하기 때문에 서버 컴포넌트에서 현재 경로를 추출하고 싶을 경우가 있을 것 같다.

 

이런 경우 미들웨어 설정을 하여 우회적으로 추출하는 방법을 소개해주는 것 같다.

미들웨어 설정하기

// middleware.js

import { NextResponse } from "next/server";

export function middleware(request) {
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-pathname", request.nextUrl.pathname);

  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

 

서버 컴포넌트에서 경로 추출하기

// 서버 컴포넌트

import { headers } from "next/headers";

export default async function Page() {
  const headersList = headers();

  return <div>{headersList.get("x-pathname")}</div>;
}

 

스택오버플로우 원문 보기

https://stackoverflow.com/questions/74584091/how-to-get-the-current-pathname-in-the-app-directory-of-next-js

 

끝.

댓글