Architecture for Next.js

곰튀김·2025년 1월 25일

backend 로직에 server action을 적극적으로 사용하기 위한 아키텍처.

문제점

  • action에서 throw 하는 exception을 처리할 수 없다.
  • logic 과 lib을 구분해야 한다.

해결방법

  • client 측과 server 측을 명확하게 나눠야 한다.
  • 서로 호출하고 반환값을 처리하는 Interface가 필요하다

Conceptual idea

  1. Controller
  • MVC에서 Controller 이름을 따왔다.
  • Client Component로써 Frontend에서 호출한다.
  • global state 값에 접근할 수 있다.
  • Service를 호출하여 backend 로직을 수행한다.
  • Service 가 반환하는 결과값을 처리한다.
  1. Service
  • Spring에서 Service 이름을 따왔다.
  • Server Component로써 server action으로 동작한다.
  • 반환값은 Plain JSON 형식으로 표현한다.
  • 다른 action logic들을 호출하며 exception을 핸들링한다.

Code Example

  1. Frontend
  • value에 대한 값의 처리를 controller에게 맡긴다.
export const Home = () => {
  const { value } = useStore();
  
  const handleClick = async () => {
    await controllerGo(value);
  }
  
  return (
    <div>
      <div>{value}</div>
      <Button onClick={handleClick}>Click</Button>
    </div>
  );
}
  1. Controller
  • service를 호출하고, 결과값에 따라 에러를 처리한다.
  • Client Side기 때문에, global state에 접근할 수 있다.
"use client";
export const controllerGo = async (value: number) => {
  const { setValue } = useStore.getState();
  
  const response = await serviceGo(value);
  if(response.result === "error") {
    console.error(response.message);
    return false;
  }
  const data = response.data;
  if(!data) {
    console.error("No Data");
    return false;
  }
  setValue(data);
  return true;
}
  1. Service
  • action 로직을 호출한다. 이 로직들은 exception을 던질 수 있으므로 catch 하여 plain JSON 으로 변환하여 반환한다.
  • Server Side기 때문데, (PUBLIC_이 아닌) environment 값을 사용할 수 있다.
"use server";
export const serviceGo = async (value: number) => {
  try {
    const ErrorProbability = process.env.ERROR_PROBABILITY;
    const newValue = await actionIncrease(value, ErrorProbability);
    return {
      result: "success",
      data: newValue
    }
  } catch (error) {
    if(error instanceof Error) {
      return {
        result: "Error",
        message: error.message
      }
    }
    return {
      result: "Error",
      message: "unknown"
    }
  }
}
  1. Action
  • backend logic 들. exception을 발생시킬 수 있다.
export const actionIncrease = async (n: number, p: number) => { 
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const randomValue = Math.random() * 100;
      if (randomValue < p) {
        reject(new Error('Random exception occurred'));
      } else {
        resolve(n + 1);
      }
    }, 1000);
  });
}

Conclusion

  • server action은 server logic 을 secure하게 처리할 수 있다.
  • 코드 관리에 용이하다.
  • 같은 레벨에서 코드가 진행하며 action이 서버코드라는 것을 간과하기 쉽다.
    • frontend에서 action을 직접 호출하여 사용한다.
    • object를 전달하려고 시도한다.
    • exception으로 에러처리를 시도한다.
  • Controller 와 Service 를 한 쌍으로 Interface를 통일하여 처리하면 역할의 구분을 명확히 할 수 있다.
profile
사실주의 프로그래머

0개의 댓글