본문 바로가기
💻 개발/toy project

json-server에서 관계형 데이터베이스 구축 및 사용 방법

by llddang 2025. 2. 25.

프론트엔드 개발 시 백엔드 API를 대체하기 위해 자주 사용되는 json-server에서, 관계형 데이터베이스를 구축하면서 겪은 에러와 이를 바탕으로 알게된 사용방법에 대해 설명해보겠다.

 

📄 개발 시나리오

사용 방법에 대해 설명하기에 앞서 앞으로 나오는 예제에 대해 간단히 설명하겠다.

예시들의 내용은 간단한 블로그 시스템을 구축하는 프로젝트로, 아래와 같은 기능이 있다.

  • 사용자는 회원가입하고 로그인할 수 있다.
  • 사용자는 글(포스트)을 작성, 수정, 삭제할 수 있다.
  • 사용자는 다른 사용자의 글에 댓글을 달 수 있다.

이 예시를 기준으로 관계형 데이터베이스를 구축하고 사용하는 방법을 알아보자!

 

🕶️ 사용 방법

1. 설치

json-server를 사용하기 위해 일단 패키지를 설치하자.

$ npm install json-server
$ yarn add json-server
$ pnpm install json-server

 

 

2. db.json

db.json 파일은 json-server의 핵심으로, 가상 데이터베이스 역할을 한다. 이 파일을 확장자에 맞게 JSON 형식으로 데이터를 정의하면 json-server가 이를 기반으로 REST API를 자동으로 생성해준다.

 

db.json 문법 및 구조

db.json 파일은 표준 JSON 형식을 따른다:

  • 최상위 객체의 각 키는 리소스(테이블) 이름이 된다
  • 각 리소스는 객체 배열 형태여야 한다
  • 각 객체는 고유한 id 필드를 가져야 한다 (자동 생성 가능)
{
  "users": [
    { "id": 1, "name": "홍길동", "avatar": "image-url" },
    { "id": 2, "name": "김철수", "avatar": "image-url" }
  ],
  "posts": [
    { "id": 1, "title": "json-server 시작하기", "userId": 1 },
    { "id": 2, "title": "관계형 데이터베이스 구축하기", "userId": 1 },
    { "id": 3, "title": "React와 함께 사용하기", "userId": 2 }
  ],
  "comments": [
    { "id": 1, "body": "좋은 글이네요!", "postId": 1, "userId": 2 },
    { "id": 2, "body": "매우 유용한 정보입니다.", "postId": 1, "userId": 1 },
    { "id": 3, "body": "감사합니다.", "postId": 2, "userId": 2 }
  ]
}

테이블(리소스) 명명 규칙

  1. 복수형 사용: 테이블 이름은 반드시 복수형으로 지정해야 한다. 예를 들어 user가 아닌 users, post가 아닌 posts로 명명해야 한다. 이는 REST API 관례를 따르는 것이며, json-server의 관계 기능이 올바르게 작동하기 위해 중요하다.
  2. 외래 키 명명 규칙: 외래 키는 참조하는 테이블의 단수형에 'Id'를 붙인 형태로 지정해야 한다. 예를 들어 users 테이블을 참조하는 외래 키는 userId여야 한다.
  3. 일관성 유지: 대소문자를 포함한 명명 규칙을 일관되게 유지해야 관계형 쿼리가 제대로 작동한다. json-server는 대소문자를 구분한다.

이러한 명명 규칙을 따르지 않으면 _embed_expand 같은 관계 쿼리 파라미터가 작동하지 않을 수 있으니 주의해야 한다.

 

 

3. 손쉬운 사용을 위해 package.json 수정

npm json-server db.json --port 4000을 통해서 json-server를 실행시킬 수 있다.
하지만 매번 위 명령어를 입력하기는 번거러움으로, 스크립트에 json-server 실행 명령어를 추가하자!

"scripts": {
  "start": "react-scripts start",
  "server": "json-server --watch db.json --port 4000",
  "dev": "concurrently \"npm run start\" \"npm run server\""
}

앞으로는 npm dev 를 통해 json-server 와 프론트 프로젝트를 실행할 수 있다.

 

 

4. API 호출 방식

Create (POST)

post 메서드를 통해 새로운 포스터를 생성할 수 있다.

// 새 포스트 생성
fetch('http://localhost:4000/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: '새로운 포스트',
    userId: 1
  })
})
  .then(response => response.json())
  .then(data => console.log('생성된 데이터:', data));

 

Read (GET)

get 메서드를 통해 포스터 목록 또는 특정 id 포스트, 필터링된 포스터 목록을 불러올 수 있다.

// 모든 포스트 가져오기
fetch('http://localhost:4000/posts')
  .then(response => response.json())
  .then(data => console.log('모든 포스트:', data));

// 특정 ID의 포스트 가져오기
fetch('http://localhost:4000/posts/1')
  .then(response => response.json())
  .then(data => console.log('포스트 1:', data));

// 필터링
fetch('http://localhost:4000/posts?userId=1')
  .then(response => response.json())
  .then(data => console.log('사용자 1의 포스트:', data));

 

Update (PUT/PATCH)

put 과 patch 메서드를 통해 특정 id의 포스트를 업데이트 할 수 있다.

  • PUT은 리소스 전체를 대체한다. 포함되지 않은 필드는 기본값 또는 null로 초기화된다.
  • PATCH는 지정된 필드만 업데이트한다. 포함되지 않은 필드는 기존 값을 유지한다.
// PUT: 전체 리소스 업데이트
fetch('http://localhost:4000/posts/1', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: '수정된 제목',
    userId: 1
  })
})
  .then(response => response.json())
  .then(data => console.log('업데이트된 데이터:', data));

// PATCH: 부분 업데이트
fetch('http://localhost:4000/posts/1', {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: '부분 수정된 제목'
  })
})
  .then(response => response.json())
  .then(data => console.log('부분 업데이트된 데이터:', data));

 

Delete (DELETE)

delete 메서드를 통해 특정 id의 포스터를 삭제할 수 있다.

// 포스트 삭제
fetch('http://localhost:4000/posts/1', {
  method: 'DELETE'
})
  .then(response => {
    if (response.ok) {
      console.log('삭제 성공');
    }
  });

 

 

5. 관계형 테이블 작성방식

json-server에서 관계형 데이터베이스를 구현하는 방법은 다음과 같다:

{
  "users": [
    { "id": 1, "name": "홍길동" }
  ],
  "posts": [
    { "id": 1, "title": "첫 번째 글", "userId": 1 },
    { "id": 2, "title": "두 번째 글", "userId": 1 }
  ]
}

 

관계 데이터 조회하기

외래 키를 사용해 관련 데이터를 조회할 수 있다:

// 특정 사용자의 포스트 조회
fetch('http://localhost:4000/users/1/posts')
  .then(response => response.json())
  .then(data => console.log('사용자 1의 포스트:', data));

// 특정 포스트와 댓글 함께 조회 (_embed)
fetch('http://localhost:4000/posts/1?_embed=comments')
  .then(response => response.json())
  .then(data => console.log('포스트와 댓글:', data));

// 특정 포스트와 작성자 정보 함께 조회 (_expand)
fetch('http://localhost:4000/posts/1?_expand=user')
  .then(response => response.json())
  .then(data => console.log('포스트와 작성자:', data));

 

주요 관계 쿼리 파라미터

  • _embed: 하위 리소스 포함 (1:N 관계에서 "1" 쪽에서 사용)
  • _expand: 상위 리소스 포함 (1:N 관계에서 "N" 쪽에서 사용)

 

참조 무결성과 연쇄 삭제

json-server는 기본적으로 참조 무결성을 유지하기 위한 연쇄 삭제(cascade delete) 기능을 제공한다.
즉, 부모 레코드가 삭제되면 그것을 참조하는 모든 자식 레코드도 함께 삭제된다.

예를 들어:

  • 사용자(users)가 삭제되면 해당 사용자가 작성한 모든 포스트(posts)도 함께 삭제된다.
  • 포스트(posts)가 삭제되면 해당 포스트에 달린 모든 댓글(comments)도 함께 삭제된다.

이 동작은 관계형 데이터베이스의 참조 무결성을 유지하는 데 도움이 되지만, 주의해야 할 점도 있다:

  1. 예상치 못한 데이터 손실: 부모 레코드를 삭제할 때 자식 레코드도 함께 삭제되므로, 중요한 데이터가 의도치 않게 삭제될 수 있다.
  2. 삭제 요청 예시:
// 사용자 1을 삭제하면 해당 사용자의 모든 포스트와 댓글이 함께 삭제됨
fetch('http://localhost:4000/users/1', {
  method: 'DELETE'
})
  .then(response => {
    if (response.ok) {
      console.log('사용자와 관련 데이터가 모두 삭제됨');
    }
  });

 

🔖 에러 해결 팁

  1. 관계형 쿼리가 작동하지 않을 때:
    • 외래 키 이름이 올바른지 확인 (예: userIdusers 테이블을 참조)
    • 단수형과 복수형 구분 (_expand=userusers 테이블을 참조)
  2. 데이터 불일치 오류:
    • 존재하지 않는 ID를 참조하는 경우 json-server는 오류를 발생시키지 않음
    • 백엔드 구현 전에 클라이언트 측에서 참조 무결성 검사 로직 구현 고려

 

 

⚠️ 내가 겪은 에러 사례

프로젝트 배경

내 프로젝트는 MBTI 테스트 애플리케이션이었다. 인증(Authentication) 관련 기능은 스파르타 코딩클럽에서 제공하는 API를 사용했기 때문에 직접 구현할 필요가 없었다.

데이터베이스 구조는 다음과 같이 설계했다:

  • users 테이블: id, nickname, avatar 정보 저장
  • results 테이블: id, type, userId 정보 저장 (테스트 결과)

🚨 첫 번째 에러: 연쇄 삭제 문제

초기에는 스파르타 코딩클럽에서 제공하는 로그인/회원가입 API를 통하여 user의 정보를 받아올 수 있어, users 테이블을 비워둔 채로 results 테이블만 먼저 구현했다:

{
  "user": [],
  "result": [
    { "id": 1, "type": "INFP", "userId": 1 },
    { "id": 2, "type": "ENTP", "userId": 1 },
    { "id": 3, "type": "ISFJ", "userId": 2 },
  ],
}

 

이 상태에서 특정 ID의 결과(result)를 삭제하려고 했는데, 예상과 달리 모든 결과가 한꺼번에 삭제되는 문제가 발생했다.

 

원인을 찾아보니 json-server의 GitHub 이슈(issue #13)에서 답을 찾을 수 있었다.

 

json-server는 참조 무결성을 유지하기 위해, 참조하는 엔티티(여기서는 userId)가 존재하지 않으면 해당 데이터를 자동으로 삭제하는 것이었다.

임시 해결책으로, users 테이블과 API 연동이 완료되기 전까지는 외래 키 이름을 userId 대신 userIdentity와 같은 다른 이름으로 변경하여 참조 관계를 끊었다.

 

🚨 두 번째 에러: 테이블 명명 규칙

users 테이블 API 연동 후, 테스트 결과와 함께 사용자 정보를 함께 불러오기 위해 _expand 쿼리 파라미터를 사용하려 했으나 예상대로 작동하지 않았다.

처음에는 문제가 없었는데 갑자기 데이터가 제대로 불러와지지 않는 현상이 발생했다.

Stack Overflow의 관련 질문을 통해 원인을 파악할 수 있었다.

 

json-server에서는 테이블 이름을 반드시 복수형으로 지정해야 관계 기능이 올바르게 작동한다는 것이었다.

테이블 이름을 단수형에서 복수형으로 수정(user → users, result → results)하니 문제가 해결되었다.

 

 

참고 자료