Ayden's journal

RAD 아키텍처를 활용한 Mock First Development

프론트엔드 개발을 하다보면 백엔드가 없는 상태에서 일단 뷰를 완성해야 할 때가 있다. 프로젝트의 극 초기라면 백엔드 인터페이스가 하루에 두 번씩 변하는 만큼 난장판도 그런 난장판이 없다. 이런 환경에서 초기 Mock 데이터를 통해 뷰를 구성하고, 이후 백엔드 인터페이스를 연결하는 일련의 과정을 나는 Mock First Development라고 부르고 있다.

이번 포스트에서는 아주아주 간단한 '프로필 기능'을 만들어보며 [ RAD 아키텍처 ] 를 활용해 안전하게 Mock First Development를 수행하는 일련의 과정을 간단히 알아보고자 한다.

 

Adaptor와 Mock

RAD 아키텍처의 A to Z는 어댑터 클래스에 있다고 해도 과언이 아니다. 우선 백엔드 인터페이스가 어떤 모습이 될 지는 모르겠지만, 점심시간에 백엔드 개발자들이랑 대화하면서 대충 uuid 형태의 id랑 email, 그리고 profileImage가 있어야 될 거라 이야기를 했다. 아주 확실하지는 않지만 자기소개를 넣을만한 짧은 bio도 포함될 것이라 생각된다. 우리의 어댑터는 여기서부터 출발한다.

type User = {id: string, email: string, profileImage: string, bio: string}

export class UserQueryAdaptor {
  static getMe = (data: User): User => ({
    id: data.id,
    email: data.email,
    profileImage: data.profileImage,
    bio: data.bio
  })
}

 

그리고 이 어댑터 클래스를 통해 mock 데이터를 작성한다.

const userMock: User = {
  id: "1",
  email: "mock@naver.com",
  profileImage: "/images/mock/mockImage.jpg",
  bio: "hello",
}

 

단순 RAD 작성

아직은 백엔드가 없기 때문에 full spec의 RAD 아키텍처는 필요하지 않다. 따라서 RAD 아키텍처의 기본적인 구조는 따르되, 아래와 같이 아주 단순한 수준으로 코드를 작성하게 된다.

abstract class Query {
  abstract queryKey: QueryKey
}

export class UserQuery extends Query {
  queryKey = ["user"]

  getMe = () => queryOptions({
    queryKey: [...this.queryKey, "me"],
    queryFn: () => userMock,
  })
}

export const useUserQuery = () => {
  const userQuery = new UserQuery()

  const GetMe = () => {
    return useQuery(this.userQuery.getMe())
  }
}
export default function Page() {
  const { data = userMock } = useUserQuery().GetMe()

  return (
    <>
      <Profile {...data}>
      <ProfileEdit {...data}>
    </>
  )
}

export default function Profile({ email, profileImage }: User) {
  return (
    <div>
      <Image src={profileImage} width={24} height={24} alt="profile" />
      <h1>{email}</h1>
    </div>
  )
}

 

이것만으로도 프론트엔드 코드를 작성하는 데는 아무런 문제도 없다. 아래와 같이 뷰가 보이니 스타일링이 들어가 있지 않아도, 데이터 흐름이나 화면 구성이 정상적으로 작동하는지를 빠르게 확인할 수 있다.

 

실제 API 연결

전체 회의 시간에 처음 확인해본 백엔드 스펙은 아래와 같다. 프로퍼티 이름에 언더바가 붙은 건 그렇다 치더라도 왜 프로필 이미지가 배열에 들어오는지 이해가 안 된다. 물어보니까 이전에 설정한 프로필 이미지도 계속 보관하면서 쓸 거라고 한다. 아하! 그런 깊은 뜻이.

type GetMeQuery = {
  id: string,
  e_mail: string,
  profile_image: Array<string>,
  bio: string,
}

 

서버에서 어떤 data가 오는지 알게 되었으니, 어댑터 클래스에 인덱스 시그니처로 처리해두었던 부분을 아래와 같이 실제 데이터 형식으로 변경해두자.

export class UserQueryAdaptor {
  static getMe = (data?: GetMeQuery): User => ({
    id: data?.id ?? "",
    email: data?.e_mail ?? "",
    profileImage: data.profile_image[0] ?? "/images/defaultImage.jpg",
    bio: data?.bio ?? "",
  })
}

 

그리고 이전에는 단순하게 작성해두었던 RAD를 실제 API와 연동하기 위한 full spec RAD로 변경해준다.

const instance: AxiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_BASE_URL,
  timeout: 5 * 1000,
  withCredentials: true,
});

export class Fetcher {
  public async doFetch<T>(config: AxiosRequestConfig): Promise<T> {
    const { data } = await instance<T>({
      ...config,
    });
 
    return data;
  }
}

export abstract class Query extends Fetcher {
  abstract queryKey: QueryKey;
 
  queryFn<T>(url: string) {
    return () =>
      this.doFetch<T>({
        method: "get",
        url,
      });
  }
 
  infiniteQueryFn<T>(url: string) {
    return ({ pageParam }: { pageParam: number }) =>
      this.doFetch<T>({
        method: "get",
        url: `${url}&skip=${pageParam}`,
      });
  }
}

 

DomainQuery에 작성한 queryFn도 mock 데이터를 제거하고 실제 실제 API 함수로 변경해주자.

abstract class Query {
  abstract queryKey: QueryKey
}

export class UserQuery extends Query {
  queryKey = ["user"]

  getMe = () => queryOptions({
    queryKey: [...this.queryKey, "me"],
    queryFn: () => this.queryFn<User>( ... ),
  })
}

 

마지막으로 useDomainQuery에 어댑터를 적용시켜주면 된다.

export const useUserQuery = () => {
  const userQuery = new UserQuery()

  const GetMe = () => {
    const result = useQuery(this.userQuery.getMe())
    return { ...result, data: UserQueryAdaptor.getMe(data)}
  }
}

 

useUserQuery().GetMe()가 리턴하는 타입은 언제나 User이다. 때문에 백엔드 인터페이스가 하루에 300번을 바뀐다 해도 프론트엔드 컴포넌트 코드는 단 한 줄 바뀌지 않는다! 이것이 RAD 아키텍처를 사용할 때의 가장 큰 이점 중 하나이다.

 

결론

RAD 아키텍처를 사용하면 백엔드가 아직 완전히 구현되지 않은 초기 단계에서도 프론트엔드 개발자가 예측 가능한 API 인터페이스를 기반으로 대부분의 코드를 미리 작성해둘 수 있다. 이를 위해 Mock 서버나 Swagger 기반의 자동 코드 생성 도구가 필요한 것도 아닌 만큼 아주 빠르게 초기 컴포넌트들을 개발할 수 있으며, 인터페이스가 확정되지 않은 상태에서도 사용자 흐름에 집중한 UI 구현이 가능하다.

또한, RAD 아키텍처는 API 응답 구조의 변경에 유연하게 대응할 수 있도록 추상화 계층을 도입하거나, 타입 안전성과 변환 유틸리티를 활용하여 인터페이스가 달라져도 최소한의 수정만으로 안정적인 시스템 동작을 유지할 수 있도록 돕는다. 이러한 접근 방식은 프론트엔드와 백엔드의 병렬 개발을 가능하게 하여 팀 전체의 생산성을 높이는 데 기여한다.

 

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기