Skip to content

프론트엔드 개발자가 알아야 할 일반적인 스킬 두 가지

등록 날짜:2024년 10월 13일 at 오후 07:30

시작하며

  1. 프론트엔드에서 기능에 맞게 데이터 표준화하기
  2. 구독 개념을 이용한 리렌더링 최적화

거창한 제목이지만 일을 하면서 경험적으로 배운 내용을 정리한 것이다.

기능에 맞게 데이터 표준화하기

프론트엔드에서 기능에 맞는 데이터 표준화는 백엔드에서 정의한 응답 DTO에 직접 의존하지 않는 이유와 여러 데이터 소스에서 데이터를 받아올 때 이를 표준화하는 과정의 중요성을 설명한다.

백엔드 API에서 제공하는 응답 DTO를 직접 의존하면 문제가 생길 수 있다.

  1. 프론트엔드 맞춤형 API는 자주 제공되지 않는다.

    • 기능 개발 시, 백엔드에서 프론트엔드에 맞춘 새로운 API를 만들어주는 일은 흔치 않다.
    • 프론트엔드 화면에 지나치게 의존하는 API 개발은 바람직하지 않으며, 때로는 이미 존재하는 API에서 필요한 데이터를 프론트엔드에서 처리하는 경우가 많다.
  2. 프론트엔드 화면은 기획에 따라 쉽게 변경된다.

    • 프론트엔드 개발자가 자유롭게 수정할 수 없는 고정된 DTO는 기능 개발을 방해할 수 있다.
  3. 백엔드의 DTO 변경에 취약하다

    • 백엔드에서 DTO가 변경되면 기존 프론트엔드 기능이 고장 날 수 있으며, 이에 의존하는 모든 컴포넌트의 코드를 수정해야 하는 경우가 발생한다.

경험상, 위 이유들 때문에 백엔드의 DTO에 직접 의존하는 것은 위험하며, 자체적으로 프론트엔드에 맞는 타입을 정의할 필요가 있다.

여러 API에 의존하는 경우

예를 들어, 여러 데이터를 한눈에 볼 수 있는 슬라이드 형태의 UI를 구현해야 한다고 가정하자. 이때 여러 API에서 데이터를 가져와야 한다면, Slide 라는 자체 타입을 정의하여 데이터를 표준화하고 관리해야 한다.

type TextSlide = {
  type: 'TEXT'
  id: number
  text: string
}
type ChartSlide = {
  type: 'CHART'
  id: number
  chartOptions: Record<string, string>
}
type TableSlide = {
  type: 'TABLE'
  id: number
  tableOptions: Record<string, string>
}
type Slide = TextSlide | ChartSlide | TableSlide

다양한 형태의 DTO가 들어오더라도, 위와 같은 Slide 타입으로 데이터를 표준화하여 처리해야 한다. 각각의 DTO를 별도로 관리하려고 하면 유지보수에 많은 어려움이 생길 수 있다.

구독 개념을 이용한 리렌더링 최적화

최근에 일을 하며 데이터 크기에 따라 체감이 될 정도로 성능이 떨어지는 문제를 경험하여 성능 최적화를 하는 방법에 대해 찾아보게 되었다.

이전에 마이크로 상태관리의 의미와 핵심 원리 글에서 구독 개념을 이용한 렌더링 최적화의 원리를 찾아서 쓴 글이 있는데 핵심은 아래와 같다.

결국 중요한 점은 필요한 상태만 구독하여 리렌더링을 최소화하는 것이다.

슬라이드 최적화 예시

import create from 'zustand';

type SlideInfo = Record<Slide['id'], Slide>;
type SlideStore = { ids: Slide['id'][]; info: SlideInfo };

const useSlideStore = create<SlideStore>(() => ({
  ids: [1, 2, 3],
  info: {
    '1': { type: 'TEXT', id: 1, text: 'text' },
    '2': { type: 'CHART', id: 2, chartOptions: {...} },
    '3': { type: 'TABLE', id: 3, tableOptions: {...} },
  },
}));

이와 같이 데이터를 표준화하여 관리한다고 가정하자.

const App = () => {
  // 슬라이드 ids 를 구독하고 있는 상태
  const slideIds = useSlideStore((state) => state.ids)
  return (
    <div>
      {/* ... 다른 컴포넌트들 */}
      {slideIds.map((id) => {
        return <Slide key={id} id={id} />
      })}
      {/* ... 다른 컴포넌트들 */}
    </div>
  )
}

구독을 이용하여 ids 상태에만 영향을 받게 만들어 슬라이드를 추가하거나 삭제하지 않는 이상, 불필요한 리렌더링을 제거할 수 있다.

const Slide = ({ id }: { id: Slide['id'] }) => {
  // slide: TextSlide | ChartSlide | TableSlide
  const slide = useSlideStore((state) => selectSlide(state.info, { id }))

  if (slide.type === 'TEXT') return <TextSlide id={id} />
  if (slide.type === 'CHART') return <ChartSlide id={id} />
  return <TableSlide id={id} />
}

Slide 컴포넌트에서는 슬라이드의 타입에 따라 적절한 컴포넌트를 선택하며, 해당 id의 슬라이드 데이터가 변경될 때만 리렌더링이 발생한다.

const TextSlide = ({ id }: { id: Slide['id'] }) => {
  // slide: TextSlide
  const slide = useSlideStore((state) =>
    selectSlide(state.info, { id, type: 'TEXT' }),
  )

  return <div>...</div>
}

const ChartSlide = ({ id }: { id: Slide['id'] }) => {
  // slide: ChartSlide
  const slide = useSlideStore((state) =>
    selectSlide(state.info, { id, type: 'CHART' }),
  )

  return <div>...</div>
}

const TableSlide = ({ id }: { id: Slide['id'] }) => {
  // slide: TableSlide
  const slide = useSlideStore((state) =>
    selectSlide(state.info, { id, type: 'TABLE' }),
  )

  return <div>...</div>
}

각 슬라이드 컴포넌트는 selectSlide 함수를 이용해 데이터를 좁혀 선택하며, 이로 인해 성능 최적화가 가능해진다.

function selectSlide<T extends Slide['type']>(
  info: SlideInfo,
  { id, type, errorMessage }: { id: number; type?: T; errorMessage?: string },
) {
  let slide: Slide | undefined = info[id]
  if (!slide) throw new Error(errorMessage)
  return slide as Extract<Slide, { type: T }>
}

만약 안전하게 selectSlide를 사용하고 싶다면, 아래와 같이 에러를 발생시키지 않는 함수를 사용할 수 있다.

function safeSelectSlide<T extends Slide['type']>(
  info: SlideInfo,
  { id, type }: { id: number; type?: T },
) {
  let slide: Slide | undefined = info[id]
  if (!!type && slide?.type !== type) slide = undefined
  return slide as Extract<Slide, { type: T }> | undefined
}

마치며

위의 두 가지 개념은 프론트엔드 개발자로서 반드시 알아야 할 중요한 스킬이다. 일을 하며 일이 너무 많아 힘들때도 있었지만, 이러한 개념을 배우고 적용하는 과정에서 많은 것을 얻었다고 느꼈고, 이를 기반으로 더 나은 품질의 개발을 목표로 해야겠다.