Recent Posts
Recent Comments
Link
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

Jwong.log

프론트엔드에서 하드 코딩 기반 데이터를 안정적으로 관리하기 with TypeScript 본문

Frontend/개발

프론트엔드에서 하드 코딩 기반 데이터를 안정적으로 관리하기 with TypeScript

NamJwong 2023. 5. 27. 20:50

서론

이번에 처음으로 하드 코딩 기반의 데이터를 통해 기능을 구현하게 되었는데, 이는 여태 주로 해오던 서버에서 데이터를 받는 것과는 고민의 관점이 다소 달랐다.

 

서버 데이터는 이미 설계된 스키마를 어떻게 구성하여 컴포넌트에 활용할지, 서버 데이터의 타입을 어떻게 검증할지 등을 고민해야 했던 반면,

하드 코딩 기반의 데이터는 스키마 구상부터 시작하여, 말 그대로 직접 사람 손으로 기입하는 데이터인 만큼 안정적으로 데이터를 유지보수 하는 방법에 대해 고민했다.

 

이는 타입 시스템을 적극 활용하여 풀어냈으며, 이 글은 그 고민의 과정을 담은 글이다.

또한 해당 글에서 소개하는 코드의 실제 사례는 아래 PR들에서 확인할 수 있다.

 

- 처음으로 해당 구조를 고안한 목 데이터 작업 PR

https://github.com/sopt-makers/sopt-playground-frontend/pull/720

 

feat: 멘토링 상세 페이지 마크업 by NamJwong · Pull Request #720 · sopt-makers/sopt-playground-frontend

🤫 쉿, 나한테만 말해줘요. 이슈넘버 close #717 🧐 어떤 것을 변경했어요~? 멘토링 상세 페이지 컴포넌트 생성 및 쿼리 파라미터 처리 로직 구현 멘토링 상세 페이지에 들어갈 하드 데이터 목업

github.com

- 개발 환경과 프로덕션 환경 각각에 다른 데이터를 사용할 수 있는 구조를 고안한 실제 데이터 작업 PR

https://github.com/sopt-makers/sopt-playground-frontend/pull/727

 

feat: 멘토링 데이터 하드 코딩 및 프로바이더 만들기 by NamJwong · Pull Request #727 · sopt-makers/sopt-playg

🤫 쉿, 나한테만 말해줘요. 이슈넘버 close # 🧐 어떤 것을 변경했어요~? 멘토링 데이터 하드 코딩 멘토링 데이터 프로바이더 만들기 🤔 그렇다면, 어떻게 구현했어요~? #720 에서 했던 작업을 기

github.com

 

히스토리 및 요구 사항

히스토리

SOPT Playground 서비스의 새 피쳐로 SOPT 멤버들 간의 '멘토링'을 주선하는 기능이 고안됐다.

이는 빠른 검증을 위해 MVP 형태로 먼저 출시하기로 했다.

그러한 배경에서 서버 작업 없이, 멘토를 직접 모집하고 각 멘토 별 멘토링 데이터를 FE 코드에서 하드 코딩하여 멘티를 모집하기로 했다.

요구 사항

위 히스토리에 따른 요구 사항을 한마디로 정리하면 다음과 같다. (이 글에서 필요한 부분만 가져와 간소화 했다.)

 

주어지는 멘토링 데이터를 하드 코딩하여 멘토링 별 멘티 모집 페이지를 만든다.

 

주어지는 멘토링 데이터 예시는 다음과 같다. (실제 데이터에서 간소화 한 버전이며, 이 예시를 계속 활용하겠다.)

 

  멘토 이름 멘토의 유저 id 멘토링 제목 멘토링 주제
멘토링 1 김멘토 10 비전공자에서 프론트엔드 개발자가 되기까지 프론트엔드 학습 전략
멘토링 2 이멘토 242 백엔드 입문자를 위한 커피챗 백엔드 커리어 패스

 

이러한 요구 사항에 맞춰 데이터를 어떻게 코드에 저장하고 관리하면 좋을까?

 

고민 1. 어떤 구조의 데이터여야 할까?

가장 먼저 했던 시도는 다음과 같다.

interface Mentoring {
  mentor: { id: number; name: string };
  title: string;
  subject: string;
}

const MENTORING_DATA: Mentoring[] = [
  {
    mentor: { id: 10, name: '김멘토' },
    title: '비전공자에서 프론트엔드 개발자가 되기까지',
    subject: '프론트엔드 학습 전략',
  },
  { mentor: { id: 242, name: '이멘토' }, title: '백엔드 입문자를 위한 커피챗', subject: '백엔드 커리어 패스' },
];

별 생각 없이 주어진 데이터를 직관적으로 코드에 옮긴 것이다.

 

하지만 요구 사항을 구현하기에 적절하지 않은 구조였다.

각 멘토 별 멘토링 데이터를 페이지 별로 사용해야 하기 때문에 이러한 단순 배열 형태가 아닌 멘토의 정보가 key가 되고 멘토링 정보가 value가 되는 객체가 필요하다.

 

따라서 다음과 같은 구조를 구상했다.

interface Mentoring {
  mentorName: string;
  title: string;
  subject: string;
}

const MENTORING_DATA: Record<number, Mentoring> = {
  10: { mentorName: '김멘토', title: '비전공자에서 프론트엔드 개발자가 되기까지', subject: '프론트엔드 학습 전략' },
  242: { mentorName: '이멘토', title: '백엔드 입문자를 위한 커피챗', subject: '백엔드 커리어 패스' },
};

 

이렇게 하면 MENTORING_DATA[mentorId].mentorName과 같은 방식으로 멘토 별 멘토링 데이터를 사용할 수 있다.

(어떤 식으로 mentorId를 받아올 것인지 까지는 이 글에서 다루지 않겠다. 어떻게든 컴포넌트에 mentorId가 주어진다고 생각하자.)

얼핏 보면 적절한 해결책 같지만, 데이터를 추가/변경/삭제하는 과정에서 다음과 같은 문제가 발생할 수 있다.

 

문제 1. 잘못된 id를 기입할 수 있다.

문제 2. id에 다른 id의 정보가 매치될 수 있다.

 

멘토의 id가 단순한 숫자라는 특성 때문에 발생할 수 있는 실수이다.

또한, 실제 데이터는 훨씬 복잡하고 양이 많기 때문이기도 하다.

 

고민 2. 어떻게 하면 하드 코딩 실수를 방지할까?

const MENTOR_DATA = [
  { id: 10, name: '김멘토' },
  { id: 242, name: '이멘토' },
] as const;
const MENTOR_ID_DATA = MENTOR_DATA.map(({ id }) => id);

type MentorId = typeof MENTOR_ID_DATA[number];

interface Mentoring {
  mentorName: string;
  title: string;
  subject: string;
}

const MENTORING_DATA: Record<MentorId, Mentoring> = {
  10: { mentorName: '김멘토', title: '비전공자에서 프론트엔드 개발자가 되기까지', subject: '프론트엔드 학습 전략' },
  242: { mentorName: '이멘토', title: '백엔드 입문자를 위한 커피챗', subject: '백엔드 커리어 패스' },
};

 

이렇게 MENTOR_DATA 배열을 만들고, MentorId 타입을 만듦으로써 위에서 언급한 두 가지 문제를 해결하려 했다.

 

MENTOR_DATA를 통해 멘토 id와 이름을 한눈에 보이는 구조로 관리함으로써 하드 코딩 시 실수할 가능성이 줄어든다.

또 MENTOR_DATA를 기반으로 MentorId 타입을 만들었으며, 이를 통해 MENTORING_DATA에 잘못된 id를 기입하는 실수를 막고자 했다.

 

아래와 같이 지정된 아이디만 key로 사용할 수 있다.

하지만 이로써 원본 데이터, MENTOR_DATA, MENTORING_DATA 이 세 가지 데이터가 각기 달라질 수 있기에 하드 코딩 방식이기에 본래 존재하던 파편화 문제는 더욱 커졌다.

 

적어도 프로젝트 코드 내에서는 데이터 정합성을 보장하도록 이 방법을 개선해볼 수 없을까?

 

고민 3. 데이터 정합성 측면 개선하기

const MENTOR_DATA = [
  { id: 10, name: '김멘토' },
  { id: 242, name: '이멘토' },
] as const;
type MentorData = typeof MENTOR_DATA[number];

type MentoringData<Mentor extends MentorData> = {
  [M in Mentor as M['id']]: {
    mentorName: M['name'];
    title: string;
    subject: string;
  };
};

const MENTORING_DATA: MentoringData<MentorData> = {
  10: { mentorName: '김멘토', title: '비전공자에서 프론트엔드 개발자가 되기까지', subject: '프론트엔드 학습 전략' },
  242: { mentorName: '이멘토', title: '백엔드 입문자를 위한 커피챗', subject: '백엔드 커리어 패스' },
};

 

이는 MENTOR_DATA를 기반으로 멘토 id와 이름을 정확하게 맵핑할 수 있는 MentoringData 타입을 만들어 해결했다.

이는 TypeScript의 Mapped Type을 활용했다.

 

아래와 같이 아이디에 따라 이름이 정확히 추론된다.

 

이 외의 다른 방법으로는 MENTORING_DATA라는 객체를 만드는 것이 아닌, getMentoringData라는 함수를 만들 수도 있을 것 같다.

 

const MENTOR_DATA = [
  { id: 10, name: '김멘토' },
  { id: 242, name: '이멘토' },
] as const;
const MENTOR_ID_DATA = MENTOR_DATA.map(({ id }) => id);

type MentorId = typeof MENTOR_ID_DATA[number];

const getMentoringDataById = (id: MentorId) => {
  switch (id) {
    case 10:
      return {
        mentorName: '김멘토',
        title: '비전공자에서 프론트엔드 개발자가 되기까지',
        subject: '프론트엔드 학습 전략',
      };
    case 242:
      return { mentorName: '이멘토', title: '백엔드 입문자를 위한 커피챗', subject: '백엔드 커리어 패스' };
    default:
      const _exhaustiveCheck: never = id;
      throw new Error(`mentor id type error`);
  }
};

이 방식의 장점은 switch 문의 default 구문에서 never 타입을 통해 빠뜨린 id가 있는지를 체크할 수 있다는 것이다.

 

또 세 번째 방법으로는, MENTORING_DATA 객체와 getMentoringData 함수를 둘 다 사용할 수도 있다.

getMentoringData만 파일에서 export 하여, 이 함수를 통해서만 MENTORING_DATA에 접근할 수 있도록 하는 것이다.

그러면 두 방식의 장점을 모두 이용할 수 있다.


+) 2023.05.29 추가

실제로 이 형태의 타입을 이용해 디벨롭을 이어나가다 알게 되었는데,

위 switch문을 이용하지 않고, MentoringData 타입만으로 빠진 id가 있는지 체크가 된다.

 

(최종 예제에서 MENTOR_DATA에 새 멘토만 추가해준 것이다.)

const MENTOR_DATA = [
  { id: 10, name: '김멘토' },
  { id: 242, name: '이멘토' },
  { id: 1, name: '박멘토' }
] as const;
type MentorData = typeof MENTOR_DATA[number];

type MentoringData<Mentor extends MentorData> = {
  [M in Mentor as M['id']]: {
    mentorName: M['name'];
    title: string;
    subject: string;
  };
};

const MENTORING_DATA: MentoringData<MentorData> = {
  10: { mentorName: '김멘토', title: '비전공자에서 프론트엔드 개발자가 되기까지', subject: '프론트엔드 학습 전략' },
  242: { mentorName: '이멘토', title: '백엔드 입문자를 위한 커피챗', subject: '백엔드 커리어 패스' },
};

멘토를 추가해놓고 멘토링 데이터에 기입을 잊으면 어떤 아이디가 빠졌는지 알려주고, 이에 따라 아이디를 추가하면 이름은 자동 추론이 되니 실제로 데이터를 추가하며 굉장히 편했다!


마무리

하드 코딩 데이터를 추가/변경/삭제하며 최대한 실수를 방지할 수 있는 구조를 만들기 위해 TypeScript를 적절히 활용해보았다.

최종 코드를 만들기까지 각종 TypeScript 기법을 이리저리 사용해보며 TypeScript에 대한 이해를 더욱 높일 수 있었다.

 

또한, 프로젝트에서 데이터를 받아서 쓰는 입장이 아니라 만들어 쓰는 입장이 되어본 것은 거의 처음이라 좋은 경험이었다.

데이터에 더 큰 주도권을 가질 수 있는 프로젝트를 하게 된다면 (like GraphQL을 사용하는 프로젝트..?) 이번에 한 고민들이 도움이 될지도 모르겠다.

 

참고로 이 방법은 현재 기능이 MVP 단계이기 때문에 최대 3-4개월 정도를 하드 코딩 방식으로 운영할 것을 염두로 두고 고안한 방법이다.

만약 정식 기능이었다면 더욱 안정적인 구조를 위해 데이터를 하드 코딩 하는 것 자체를 최대한 회피하기 위해 고민했을 것이다.