프론트엔드와 백엔드 타입을 하나로! 공용 타입 SDK 구축하기

영자이다·2024년 11월 10일
post-thumbnail

들어가며

흔하지는 않지만, 프론트엔드와 백엔드가 통일된 하나의 타입을 사용해야하는 경우가 있다.
프론트엔드와 백엔드가 사용하는 데이터 타입을 일관되게 관리하는 것은, 특히 그 구조가 복잡한 경우에는 꽤나 필수적인 과제다.

사내에서 Landwich라는 서비스의 웹 빌더 개발을 진행할 때, 프론트엔드와 백엔드가 동일한 Page 타입을 사용해야 하는 상황이 있었다. 이 글에서는 해당 서비스에서 프론트엔드와 백엔드가 공통으로 사용한 Page 타입을 예시로, 타입 일관성 관리의 중요성과 이를 해결하기 위한 SDK 도입 과정을 다루고자한다.

문제 배경 : Page 타입의 복잡성과 타입 불일치 문제

Page 타입의 구조

Landwich에서는 자체적인 웹 빌더를 통해 일종의 비교 페이지를 생성한다.
백엔드에서는 유저가 해당 페이지를 처음 만들때 해당 페이지의 wire-frame의 형태로 초기값을 생성하여 저장해주고. 프론트엔드는 그렇게 생성된 초기값을 바탕으로 페이지를 렌더링해 주거나, 페이지를 수정하는 등의 기능을 구현해야했다.

그렇기에 프론트엔드와 백엔드에서 사용하는 Page의 구조가 반드시 일치해야만 했다. (프론트엔드와 백엔드 모두 typescript를 사용했다)

여기서 프론트엔드와 백엔드가 핵심적으로 사용하는 것이 Page 타입이다.
Page 타입은 여러 Section으로 구성되며, 각 Section은 해당 Section에 포함되는 다양한 Image, Text, Button, Flex 요소들을 nodes라는 이름의 배열로 가지고 있다.

예를 들면

위는 서비스에서 생성하는 HeroSection의 wire-frame 형태이다.
해당 HeroSection의 데이터 구조는 다음과 같다

- Section
	- Flex
		- Text
		- Text
		- Button
		- Flex
			- Image
			- Image

해당 구조는 아래와 같은 형식으로 관리된다

// HeroSection
{
	...
	nodes: [
		Text,
		Text,
		Button,
		nodes: [
			Image,
			Image
		]
	]
}

즉, HeroSection은 Text 두 개, Button, Image 두 개가 묶여있는 Flex가 배열로 저장되어있는 형태이다. 보면 알 수 있지만 nodesFlex일 때만 존재한다.
이는 서비스내에서 가장 간단한 형태의 Section이고, 다른 Section 들의 경우에는 더 복잡한 구조를 가지고 있었다.

위는 IntroSection 의 wire-frame 형태이다.
데이터 구조는 아래와 같다

- Section
	- Flex
		- Flex
			- Text
			- Text
		- Flex
			- Text
			- Flex
				- Flex
					- Image
					- Text
				- Flex
					- Image
					- Text
				- Flex
					- Image
					- Text

해당 데이터 구조는 다음과 같은 형식으로 관리된다

// IntroSection
{
	...
	nodes: [
		nodes:[
			Text,
			Text
			],
		nodes: [
			Text,
			nodes:[
				nodes: [
					Image,
					Text
				],
				nodes: [
					Image,
					Text
				],
				nodes: [
					Image,
					Text
				]
			]
		]
	]
}

구조를 보면 알 수 있듯, Flex를 포함한 nodes 배열은 중첩된 nodes 구조를 가질 수 있는데, 경우에 따라서 이 구조가 굉장히 복잡해지기도 했다. 하지만 구조가 복잡하더라도, 모든 Section이 동일한 규칙을 따르는 형태를 유지해야 했기 때문에, 이러한 규칙을 만족하는 타입을 정의하는 것이 중요했다.
이를 위해 프론트엔드와 백엔드 개발자들이 함께 논의하여 컨벤션을 마련했고, Section별 타입을 정의한 컨벤션을 Notion에 문서화하여 해당 타입을 공유했다.

문제 상황

하지만 이렇게 결정된 타입을 바탕으로 개발을 하는 과정에서, 예기치 못하게 타입이 수정되어야만 하는 상황이 종종 일어났다.
이렇게 변경 사항이 발생할 때면, 수정 방향을 쉽게 파악할 수 있도록 Notion에 정리해둔 문서를 업데이트 해주었다.

그 과정은 다음과 같았다.

  1. 백엔드 단에서 수정사항 발생!
  2. Notion의 문서를 업데이트
  3. 프론트엔드에 변경사항 공유
  4. Notion에 업데이트된 타입을 바탕으로 프론트엔드에서 선언한 타입을 수정해준다
  5. 문제 해결~

하지만 이런 방식은 꽤나 번거롭고 귀찮았다.
프론트엔드와 백엔드의 코드가 하나의 레포에서 관리되고 있는 것이 아니었기 때문에, 수정이 생기면 프-백 각 팀의 개발자가 모두 값을 수정해주어야했다.
휴먼 팩터에 타입의 일관성을 의지해야하는 상황이 되어버린 것이다! ㅜㅠ

그러다보면 다음과 같은 웃픈 상황도 연출되곤 했다

: "백엔드에서 받아온 값으로 page를 렌더링하는데 타입 에러가 생기는데 한 번 확인해주실 수 있나요?"

: "헉ㅜㅠ 알겠습니다"

...
(디버깅)
...

: '도저히 원인을 못찾겠네 ㅠ'

: "앗! 제가 page 타입을 수정했는데 전달을 못했네요ㅠ 지금 notion에 업데이트 해둘게요"

: 🙂🔫

문제 정리

개발 과정중에 새로운 요구사항이 발견되거나 변경되어 Page의 타입을 수정해야하는 경우에 느낀 문제점들은 다음과 같았다.

  1. 번거로운 작업으로 인한 작업 효율 저하

    • 매번 프론트엔드와 백엔드가 각각 수동으로 타입을 수정해야만 한다
    • 이는 매우 귀찮은데다가. 구조가 복잡한 경우 실수가 생길 가능성도 크다
  2. 휴먼 에러 발생 가능성

    • 아무래도 매번 직접 타입을 수정해주어야하다보니, 위의 예시처럼 타입의 수정이 누락되거나 프론트엔드와 백엔드간의 타입이 일치하지 않는 일이 종종 발생했다.
    • 이렇게 실수로 인한 에러가 발생한다면 불필요한 디버깅에 시간이 소요되었기 때문에 작업 속도나 효율에 영향을 미칠 수 밖에 없었다.
    • 또한 Page 타입은 서비스의 핵심 기능과 밀접하게 연관되어 있기 때문에, 타입 오류가 발생하는 것은 서비스에 치명적이었다.

위의 문제를 해결하기 위해서, 나는 공통으로 사용하는 타입을 편하게 사용할 수 있는 방법을 고민해보게 되었다.

타입 공유를 위한 SDK 제작 및 도입 과정

Github Packages로 SDK를 제작/배포

생각해낸 방식은 공통으로 사용하는 타입을 SDK로 제작하는 것이었다!
프론트엔드와 백엔드에서 해당 SDK를 import 하여 사용한다면, 타입의 수정이 있을때 SDK의 버전만 업데이트해주면 IDE 단에서 타입 불일치를 미리 확인하고 처리해줄 수 있을 거라는 생각이 들었기 때문이다.

이를 위해서 해당 타입들을 정의하는 별도의 레포를 생성하여 작업을 진행해주었다.
패키지를 위한 타입을 생성하는 것은 어렵지않았는데, 프론트엔드와 백엔드에서 이전에 협의한 컨벤션을 따라 타입을 생성해주었다.

이렇게 생성한 타입은 npmjs 를 통해 패키지를 만들고 배포할 수 있다.
하지만 npmjs 는 비공개 레포의 패키지 지원에 대해서는 유료 플랜을 사용해야하기 때문에, github packages를 사용하기로 했다.
github packages를 사용하기 위해 해당 Github DOCS를 참고하여 작업을 했다.

Github Actions을 사용한 배포 자동화 및 버전 관리

배포 자동화

중요한 것은, 프론트엔드 개발자든 백엔드 개발자든 해당 타입을 수정할 수 있어야한다는 것이었다.
그렇기 때문에 Github Actions을 사용하여, 수정사항을 반영한 package를 만들고 배포하는 과정을 자동화 처리를 해두었다.

업데이트하는 과정은 다음과 같다.

  1. 수정이 필요한 경우, 해당 레포에서 branch를 생성하고 수정사항을 반영한 뒤 PR을 날린다.
  2. 수정사항 리뷰를 받고 main에 머지되어 PR이 merge-closed 된 경우, package를 제작하고 배포하는 github actions을 트리거한다

Versioning 이슈

이 방식대로 작업을 하다보니 추가적인 문제가 생겼는데,
수정사항을 반영하여 package를 제작해서 배포하는 경우 package.json의 version을 직접 수정을 해주어야했다.

만일 누군가가 수정 사항을 배포하면서 해당 버전을 업데이트 하지 않는다면, 배포가 제대로 되지 않거나 버전을 구분하기 힘든 상황이 생기게 되었다.

이를 위해서 Github Actions을 사용하여 package를 배포할 경우, 자동으로 버전을 업데이트 해주는 작업까지 추가해주었다.

버전은 vx.y.z 형태로 배포시마다 가장 하위의 z가 1씩 업데이트 되도록 컨벤션을 맞추었다.
가장 하위의 버전(z)은 배포마다 자동으로 버전업이 1씩 증가되지만.
그보다 상위 버전의 배포가 필요할 때에는 x 또는 y를 직접 수정하여 배포하도록 만들어주었다.

결과적으로 위와 같은 과정을 통해서,
프론트엔드와 백엔드가 똑같이 사용하는 타입을 각각의 환경에서 관리해주는 것이 아니라,
해당 타입을 Git 패키지로 SDK를 생성하고 npm과 연결하여 프로젝트에서 쉽게 설치 가능하도록 통합했다.
또한 Git Actions을 활용해 main 브랜치로 머지될 때마다 자동으로 새로운 버전을 릴리즈하도록 배포 자동화를 구축하였다.

SDK 도입 결과

공통으로 사용하는 타입에 수정사항이 생긴 경우, 우리의 작업 과정은 다음과 같이 바뀌게 되었다

: '초기값을 생성하다보니 특정 Section의 구성이 조금 바뀌어야할 것 같네'

...
(타입 수정후 merge하여 SDK Package 업데이트)
...

: 수정사항이 생겨 타입 SDK 업데이트했습니다! 최신 버전으로 업데이트해주세요~

: 넵

: '수정된 타입에 맞춰서 안맞는 값만 바꿔주면 끝나겠네👍👍'

결론

이번 글에서는 프론트엔드와 백엔드가 공통으로 사용하는 타입을 SDK로 통합하여 관리한 경험을 소개해보았다.
SDK를 도입한 후, 앞서서 설명했던 문제점 두 가지를 전부 해소할 수 있게 되었다.

  1. 번거로운 과정이 전부 사라졌다
    • 기존에는 수정사항이 있을때마다 문서를 통해 공유하고, 이에 맞게 백엔드와 프론트엔드가 각각 저장소에서 관리하는 타입을 문서에 맞게 업데이트 해주어야했다.
    • 하지만 SDK를 사용함으로써 각각의 팀에서 수동으로 타입을 관리할 필요가 없어져 작업의 효율과 개발 속도가 훨씬 빨라질 수 있게 되었다.
  2. 타입 불일치로 인한 휴먼 에러 최소화
    • SDK로 공통의 타입을 관리하게되면서 수동으로 타입을 수정하던 기존 방식과 비교했을 때 타입 불일치와 누락으로 인한 오류가 크게 감소하게 되었다.
    • 위의 SDK 도입 결과에서 설명한 사례처럼, 수정사항 발생 시 SDK 업데이트만으로 프론트와 백엔드에 자동 반영됨으로써 휴먼 에러를 방지 할 수 있었기 때문이다!

물론 이 방식은 상황에 맞게 잘 사용하는게 좋아보인다.

Page와 같이 복잡한 타입의 일관성 유지가 중요한 프로젝트에서는, 공통 타입을 SDK로 관리하는 것이 유용했고 이를 통해 얻을 수 있는 장점이 뚜렷했다.
해당 SDK 도입 이후 동료 개발자 분들의 반응도 꽤나 좋았다.

하지만, 급한 상황이거나 사소한 수정사항만 있는 경우에도 패키지를 업데이트 해주어야 했기 때문에 불편할 때가 있긴 했다.
공통으로 사용되는 타입이 많지 않거나 비교적 덜 중요하다면, 이전처럼 각자가 타입을 관리하는 것이 더 효율적일 수도 있다는 생각을 했다.

또한 우리는 타입을 사용한 이후에, 큰 틀의 수정은 많지 않았기 때문에 버전 관리를 앞서 설명한 정도로만 해도 큰 문제가 생기지 않았었다.
만일 타입이 크게 바뀌는 경우에는 버전 관리가 좀 더 까다로워 질 수 도 있을 것 같은데 그 상황에서는 버전 관리를 어떤식으로 하는게 좋을지에 대한 고민이 더 필요해보기이도 한다.


p.s. 만일 이 방식 외에 더 좋은 방법이 있다면 언제든 댓글로 의견을 부탁드립니다!

0개의 댓글