본문 바로가기
다국어 이상형 월드컵 프로젝트

다국어 지원 번역파일 db 설계(planetscale, prisma)

by 우주속공간 2023. 10. 1.

왜 데이터베이스를 설계해야 할까?

 

비용 효율적이기 때문이다

그렇다면 좋지 않는 설계는 ➡️ 많은 시간과 비용이 소요되어 비효율적이다

예) 작은 부분이 변경되었음에도 불구하고 기존 db에 많은 변경이 일어나야한다.

→ 작은 변경을 수용하기 위해 많은 시간과 비용 소요

예) 고객이 서비스를 이용하는데 시간이 너무 많이 걸려 서비스 이탈

→ 수익 감소로 이어짐

비용적으로 가장 효율적으로 운용하기 위해서는 각 상황에 맞은 효율적인 데이터베이스가 필요하다

 

좋은 데이터베이스 설계란?

 

효율적인 데이터베이스 설계를 위해서 고려해야될 사항들은 다음과 같다.

  1. 무결성 : 데이터베이스 내에 모든 값은 언제나 정확한 값을 유지해야한다.
  2. 유연성 : 데이터베이스 구조는 요구사항 변화에 대해 수정이 쉬워야 한다.
  3. 확장성 : 데이터베이스 구조는 확장에 대해서 수정이 쉬워야한다.

++ 성능, 보안, 효율성 고려

 


 

번역 파일 DB 설계

 

데이터 파일 저장방식은 변경 가능성에 따라 생각해보았다.

 

1️⃣ 자주 업데이트되지 않거나 고정된 버튼 글자 같은 경우에는 변경 가능성이 없다고 보고 기존 json 파일에 두었다.

2️⃣ 게임 데이터와 같이 자주 업데이트 되거나 수정되는 데이터들은 변경 가능성이 있다고 판단되어 db를 설계하여 저장했다.

 

| 변경가능성이 없는 고정 데이터들(버튼, header등) - json 파일

| 변경 가능성이 있는 게임 데이터들을 따로 저장 - db에 저장

 

게임 데이터 중 다중 언어 파일이 존재하는 항목 ⇒  게임 타이틀, 설명, 이미지 이름

 

설계 방법

 

1️⃣ 제공하는 언어별로 스키마에 속성을 추가해준다

 

//game table

{
	'id':'1',
	'gameNameKo:'남자아이돌월드컵',
	'gameNameEn':'boyidol'
}

 

장점 : 데이터베이스 구조가 복잡해지지 않는다, 단순하다

단점 : 언어 설정에 따라 ‘gameNameKo’, ‘gameNameEn’으로 접근해야한다.

 

프로젝트에 적용해본다면...

 

db 구조가 단순하다는 장점이 있지만

  • 게임 이름, 설명, 각 대표 이미지 이름까지 저장해야된다
  • 영어,한글뿐만아니라 다른 언어들로 추가될 경우 하나의 열에 너무 많은 데이터가 들어가게 된다.
  • 클라이언트 또는 api 단에서 별도의 처리가 필요하다

 

2️⃣ 언어별 테이블을 별도로 구성해준다

 

// game_ko table
{
	'id':'1',
	'gameNameKo:'남자아이돌월드컵'
}

//game_en table
{
	'id':'1',
	'gameNameEn':'boyidol'
}

 

장점 : 별도의 처리 없이 필요시 번역 파일에 바로 접근해서 사용가능하다.

단점 : 데이터베이스 구조가 복잡해지고 테이블이 2배로 늘어난다.

데이터 베이스를 여러개 만들어야하고 처음 설계할때 다른 db와 관계 맺기가 복잡하지만 가장 직관적으로 데이터를 저장할 수 있고 번역 파일에 접근하기에 가장 편리하다.

 

프로젝트에 적용해본다면...

  • 게임, 이미지 db를 번역 언어에 따라 따로 생성해야한다.
  • comment와 image가 gameId를 기준으로 관계가 형성되어있기때문에 그 기준이 되는 game table과 지원 언어에 따라 gameKo, gameEn를 생성하였다.

 

3️⃣ 번역에 필요한 데이테들을 translation table에 넣어두고 연동해서 쓴다

 

// game table
{
	'id':'1'
}

// translation table
{
	'id':'1',
	'gameId':'1',
	'ko':'남자아이돌월드컵',
	'en':'boyIdol'
}

 

장점 : 지원하고자 하는 언어가 추가가 될 경우 테이블을 만들거나 다른 테이블을 건드리지 않고 이 부분만 수정하면 된다.

단점 : 데이터베이스 구조와 쿼리문이 복잡해진다.

 

하나의 테이블안에 번역 데이터를 다 넣어서 보관하기 때문에 편리하지만

 

프로젝트에 적용해본다면

  • 게임과 이미지 데이터 저장 방식이 달라서 game, image translation 테이블을 따로 만들어야된다.
  • 알맞는 데이터를 사용하기 위해 별도의 처리가 필요하다.

 

따라서 언어별 테이블을 따로 만드는 2번의 방법을 선택하기로 했다.

하나의 데이터 안에 번역해야하는 데이터가 꽤 있다는 점과

현재로써는 번역해야하는 파일이 game과 image만 존재해서 테이블을 언어별로 늘리더라도 엄청 많아지지는 않을 것 같아서 2번의 방법을 선택했다.

 

만약 game,image말고 번역해야하는 것들이 늘어난다면? 번역 지원 언어가 늘어난다면? 각 언어별 table을 매번 생성하는 것보다 translation table을 만들어서 용도에 따라 생성하는것이 더 나을지도 모르겠다. translation_game, translation_image와 같이...

 

구현 코드

//image와 comment 테이블과 관계를 맺을때 기준이 되는 키를 제공해주기 위해서 main 테이블인 Game을 생성

model Game {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  totalCount Int @default(0)
  Images Image[]
  GameKo GameKo[]
  GameEn GameEn[]
  comments Comment[]
}

//game 한국어 번역파일이 있는 GameKo 생성 gameId 기준

model GameKo {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  gameTitle     String?
  content   String?
  leftName     String? @db.Text
  rightName   String?   @db.Text
  leftImage String? @db.Text
  rightImage    String? @db.Text
  game      Game?     @relation(fields: [gameId], references: [id])
  gameId    Int?
  @@index([gameId])
}


//game 영어 번역파일이 있는 GameEn 생성 gameId 기준

model GameEn {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  gameTitle     String?
  content   String?
  leftName     String? @db.Text
  rightName   String?   @db.Text
  leftImage String? @db.Text
  rightImage    String? @db.Text
  game      Game?     @relation(fields: [gameId], references: [id])
  gameId    Int?
  @@index([gameId])
}

 

처음 데미데이터 저장하는 seed 코드도 변경된 db에 맞게 변경

const load = async () => {
  try {
    await prisma.game.deleteMany();
    await prisma.gameKo.deleteMany();
    await prisma.gameEn.deleteMany();
    console.log('Deleted records in game table');

    await dataKo.map(async (n: any) => {
      await prisma.game.create({ data: { id: n.id } });
    });
    await prisma.gameKo.createMany({ data: dataKo });
    await prisma.gameEn.createMany({ data: dataEn });
    console.log('Added game data');

    await prisma.image.deleteMany();
    console.log('Deleted records in image table');

    await prisma.image.createMany({
      data: imageList,
    });
    console.log('Added image data');
  } catch (e) {
    console.error(e);
    process.exit(1);
  } finally {
    await prisma.$disconnect();
  }
};

load();

 

main페이지 데이터 로딩 사용방식도 변경

  {dataList.map((data: any) => {
            return (
              <>
                <Link href={lang + '/' + encodeURI(data.id) + '/tournament'}>
                  <div className="transition duration-150 h-full max-w-sm hover:ease-in-out hover:scale-110  relative group overflow-hidden  bg-white border border-gray-200 rounded-lg shadow ">
                    <div className=" z-10 absolute flex justify-center items-center bg-black w-full h-full rounded-xl bg-opacity-50 opacity-0 group-hover:opacity-100 ">
                      <h4 className=" text-white font-bold text-2xl ">
                        {t('game.start')}
                      </h4>
                    </div>
                    <div className="min-w-full p-2">
                      <h2 className="text-lg font-bold tracking-tight text-gray-900">
                        {data.gameTitle}
                      </h2>
                      <h4> {data.content}</h4>
                    </div>
                    <div className="grid grid-cols-2 object-center h-5/6  overflow-hidden z-5 ">
                      <div className="">
                        <img className=" h-full" src={data.leftImage} alt="" />
                        <p className=" absolute bottom-3 py-2 flex w-1/2  justify-center font-bold text-white">
                          {data.leftName}
                        </p>
                      </div>
                      <div className="">
                        <img
                          className=" h-full "
                          src={data.rightImage}
                          alt=""
                        />
                        <p className=" absolute bottom-3 p-2  flex w-1/2  justify-center font-bold  text-white">
                          {data.rightName}
                        </p>
                      </div>
                    </div>
                  </div>
                </Link>
              </>
            );
          })}

 

궁금했던 점

- 변경 가능성이 적은 파일 같은 경우에도 db에 저장하는것이 맞는지?

- 게임이 새롭게 만들어질때마다 번역 파일을 어떻게 생성할 것인지.

- 생성자가 번역 파일 직접 업로드하는 방식? 번역 API를 사용하는 방식?

- 어디까지 번역해야하는지