getServerSideProps에 Wrapper 적용하기

tunggary·2023년 1월 5일
0

조인히어

목록 보기
1/1
post-thumbnail

Wrapper를 만드는 이유

Next.js 프로젝트를 진행하던 중getServerSideProps, getStaticProps 함수 관련해서 모든 페이지에서 동일한 규격으로 함수 내부에서 데이터를 fetching하고, cookie를 이용하여 인증과 필요에 따라 redirect도 진행한다. 이러한 부분에서 각 페이지 별로 이 함수를 작성할 때 로직이 중복이 되지만 서로 다르게 구현이 되어 관리 측면에서 복잡해지고, 새로운 페이지를 확장할 때도 중복된 코드를 반복적으로 작성하게 되어 생산성이 떨어졌다. 따라서 getServerSideProps, getStaticProps를 감싸는 Wrapper 함수를 만들어 공통적으로 처리해야하는 로직을 처리해주려고 한다. 또한 Wrapper를 이용하여 공통 로직을 처리하는 이유는 모든 페이지에서 Wrapper에서 처리할 공통 로직을 분리하고, 페이지 마다 필요한 데이터 fetching만 각 페이지에서 담당할 수 있도록 하기 위함이다.

문제 상황 및 원인 파악

// 페이지 1
export async function getServerSideProps(ctx) {
  const { id } = cookies(ctx);
  const loginInfo = {
    isLoggedIn: !!id,
    userId: id
  }

  //cookie 확인 후 token이 있으면 홈화면으로 redirect
  if (id) {
    return {
      redirect: {
        destination: "/",
        permanent: false,
      },
    };
  } else {
    return {
      props: loginInfo,
    };
  }
}

// 페이지 2
export async function getServerSideProps(ctx) {
  const { id } = cookies(ctx);
  const loginInfo = {
    isLoggedIn: !!id,
    userId: id
  }
  const { clubid: clubId } = ctx.params;
  const isManager = await isManager(clubId, id);
  if (isManager) {
    const { data } = await axios.get(`http://3.36.36.87:8080/clubs/${clubId}/applications`);
    return {
      props: {
        ...loginInfo,
        clubId,
        data,
      },
    };
  } else {
    return {
      redirect: {
        destination: "/manage",
        permanent: false,
      },
    };
  }
}

// 페이지 3
export async function getServerSideProps(ctx) {
  const { id: userId } = cookies(ctx);
  const loginInfo = {
    isLoggedIn: !!id,
    userId: id
  }
  const { update, clubId } = ctx.query;

  if (!userId) {
    return {
      redirect: {
        destination: "/",
        permanent: false,
      },
    };
  }

  if (update) {
    const isManager = await isManager(clubId, userId);
    if (!isManager) {
      return {
        redirect: {
          destination: "/manage",
          permanent: false,
        },
      };
    } else {
      const { data } = await axios.get(`http://3.36.36.87:8080/clubs/${clubId}`);
      return {
        props: {
          ...loginInfo,
          defaultInfo: data.club,
        },
      };
    }
  } else {
    return {
      props: loginInfo,
    };
  }
}

위의 페이지 3개의 코드를 보고 문제점과 원인을 찾아보면

첫번째 getServerSideProps, getStaticProps는 Next.js에서 제공하는 서버 사이드에서 실행하는 함수이기 때문에 return 형식을 맞춰줘야 한다. 따라서 redirect가 필요한 경우에 따라 다음과 같이 복잡하게 분기문을 타게 되어 코드가 길어지고, 가독성이 떨어지게 된다.

두번째 페이지에 로그인 관련 정보를 서버 사이드에서 넘겨줘야 하기 때문에 공통적으로 context의 cookie를 가져와 로그인 정보를 props로 전달하는 모습을 볼 수 있다.

해결과정

관심사 분리

우선 각 페이지에서는 getServerSideProps와 getStaticProps를 통해 페이지에서 필요한 작업(데이터 fetching 등등)만 할 수 있도록 나머지 로직을 Wrapper함수로 분리하는 작업을 진행했다. 우선 Wrapper 함수의 틀을 짜보면 매개 변수로 callback 함수를 전달 받아 페이지 별로 필요한 데이터를 fetching 하고, Wrapper에서는 페이지에 전달할 props를 포함한 객체를 return 한다.

const ssrWrapper = (callback) => {
  return async (context) => {
    let returnData = {
      props: {}
    };
    returnData.props = { ...returnData.props, ...(await callback({ context })) };
    return returnData;
  };
};

export default ssrWrapper;
// 페이지에서 Wrapper 사용
export const getServerSideProps = ssrWrapper(async () => {
    const { data } = await axios.get("http://3.36.36.87:8080/clubs");
    return { data };
});

인증 관련 처리

사용자에 대한 정보(userId)가 필요한 페이지가 있으므로 Wrapper에서 cookie값을 뽑아내어 callback 함수의 매개변수로 전달한다. 추가적으로 props에 로그인 관련 정보도 전달한다.

const ssrWrapper = (callback) => {
  return async (context) => {
    let returnData = {
      props: {
        isLoggedIn: false,
        userId: null,
      }
    };
    // cookie값 추출
    const { id: userId } = cookies(context);
    // 로그인이 되어있으면 로그인 관련 정보 설정
    if (userId) {
      returnData.props.isLoggedIn = true;
      returnData.props.userId = userId;
    }
    returnData.props = { ...returnData.props, ...(await callback({ context, userId })) };
    return returnData;
  };
};

export default ssrWrapper;
// 페이지에서 Wrapper 사용
export const getServerSideProps = ssrWrapper(async ({ userId }) => {
  const { data } = await axios.get(`http://3.36.36.87:8080/members/${userId}/belongs`);
  return { data };
});

Redirect 처리

이 부분을 어떻게 처리할지에 대해서 많은 고민을 했던거 같다. 우선 Wrapper에서 redirect를 전적으로 처리하기에는 페이지에서 처리하는 경우에 따라 redirect 여부와 경로가 달라지는 경우가 많았다. 따라서 redirect가 필요한 경우 callback 함수에서 특정 flag를 만들어 Wrapper에서 flag에 따라 redirect를 처리했다.

const ssrWrapper = (callback) => {
  return async (context) => {
    let returnData = {
      props: {
        isLoggedIn: false,
        userId: null,
      }
    };
    const { id: userId } = cookies(context);
    if (userId) {
      returnData.props.isLoggedIn = true;
      returnData.props.userId = userId;
    }
    
    const callbackReturn = await callback({ context, userId });
    // flag에 따라 redirect 처리
    if (!!callbackReturn.redirect) {
      return {
        redirect: {
          destination: callbackReturn.redirect,
          permanent: false,
        }
      }
    }                                    
    returnData.props = { ...returnData.props, ...callbackReturn };
    return returnData;
  };
};

export default ssrWrapper;
// 페이지에서 Wrapper 사용
export const getServerSideProps = ssrWrapper(async ({ userId }) => {
  // redirect flag 설정하여 리턴
  if (!userId) return { redirect: '/login' }
  
  const { data } = await axios.get(`http://3.36.36.87:8080/members/${userId}/belongs`);
  return { data };
});

하지만 이럴 경우 callback에서 flag의 변수명을 잘못 설정하면 오류가 발생하지 않고 페이지가 그대로 보여지게 되는 등 callback에 의존적이게 되어 좋은 방법 같지가 않았다. 따라서 redirect가 필요한 경우 callback 내에서 error를 터뜨려 wrapper에서 error를 받아 redirect를 처리하도록 했다. 이 경우 flag의 변수명을 잘못 설정하여도 페이지가 그대로 보여지는 것이 아닌 500페이지가 보여지도록 하였다.

const ssrWrapper = (callback) => {
  return async (context) => {
    let returnData = {
      props: {
        isLoggedIn: false,
        userId: null,
      }
    };
    try {
      const { id: userId } = cookies(context);
      if (userId) {
        returnData.props.isLoggedIn = true;
        returnData.props.userId = userId;
      }
      returnData.props = { ...returnData.props, ...(await callback({ context, userId })) };
      return returnData;
    } catch (error) {
      //에러가 발생한 경우 redrect error 인지 확인, 아닌 경우 500페이지로 redirect
      const redirectUrl = isErrorRedirectable(error) ? error.redirect : '/500';
      return {
        redirect: {
          destination: redirectUrl,
          permanent: false,
        },
      };
    }
  };
};

const isErrorRedirectable = (error) => {
  return typeof error === "object" && Object.prototype.hasOwnProperty.call(error, "redirect");
};

export default ssrWrapper;
// 페이지에서 Wrapper 사용
export const getServerSideProps = ssrWrapper(async ({ userId }) => {
  // redirect error 발생하도록 함
  if (!userId) throw { redirect: '/login' };
  
  const { data } = await axios.get(`http://3.36.36.87:8080/members/${userId}/belongs`);
  return { data };
});

결과

결과적으로 각 페이지에서 사용하는 코드의 양을 줄일 수 있었고, 관심사도 분리할 수 있어 관리도 편해졌다. 또한 확장시에도 중복 로직을 작성할 필요가 사라졌다.

수정 후 코드

// 페이지 1
export const getServerSideProps = ssrWrapper(async ({ userId }) => {
  if (userId) {
    throw { url: "/" };
  }
});

// 페이지 2
export const getServerSideProps = ssrWrapper(async ({ context, userId }) => {
  const { clubid: clubId } = context.params;

  if (!userId) throw { redirect: "/login" };
  if (!(await isManager(clubId, userId))) throw { redirect: "/manage" };

  const { data } = await axios.get(`http://3.36.36.87:8080/clubs/${clubId}/applications`);

  return { clubId, data };
});

// 페이지 3
export const getServerSideProps = ssrWrapper(async ({ userId, context }) => {
  const { update, clubId } = context.query;

  if (!userId) throw { url: "/login" };

  if (update) {
    if (!(await isManager(clubId, userId))) throw { url: "/manage" };

    const { data } = await axios.get(`http://3.36.36.87:8080/clubs/${clubId}`);

    return { defaultInfo: data.club };
  }
});

추가 개선 사항

현재는 서버 사이드 실행 함수로 getServerSideProps만 사용하여 getStaticProps를 사용할 때를 고려하여 Wrapper를 작성해야 할 것 같다. 현재 상황으로 로그인 여부를 단순히 cookie 값의 여부로 하고 있어 getStaticProps를 사용하려면 인증 로직과 같이 수정을 해야할 것 같다(getStaticProps는 빌드타임에 실행되기 때문에 cookie를 추출 못함)

0개의 댓글