JS/TIL(Today I Learned)

2024-12-04

프린스 알리 2024. 12. 4.

REST API 개발과 CRUD

REST API란 무엇일까? 내가 이 개념을 처음 접했을 땐 굉장히 어렵게 느껴졌다. 왜였을까 생각을 해봤더니, 이 단어가 특정한 구조의 API를 지칭할 것이란 착각 때문이었다.

 

사실, REST API는 방법론이다. 어떠한 API 설계 원칙에 대해 말할 때, REST API라는 이름을 붙여놓은 것이다.

 

그렇다면 그게 대체 어떤 방식이길래 이름까지 붙여진 걸까. REST는 "Representational State Transfer"의 약자이므로, 자원(Resource)을 정의하고 이 자원에 대한 작업을 수행하는 방식이라고 해석할 수 있겠다.(여기서 자원은 웹에서 다루는 데이터나 정보를 의미한다.) 자원을 URI로 정의하고, HTTP 메서드를 통해 CRUD 작업을 수행하는 구조이며, 이를 통해 클라이언트와 서버 간의 데이터 전송이 효율적이고 명확하게 이루어질 수 있다.

REST API의 기본 개념

  1. 자원(Resource): 웹 API에서 다루는 데이터의 단위로, 예를 들어 사용자, 게시글, 사진 등이 될 수 있다. 각 자원은 고유한 URI(Uniform Resource Identifier)를 통해 접근된다.
  2. HTTP 메서드: REST API는 HTTP 프로토콜을 기반으로 하며, 자원에 대한 작업을 수행하기 위해 다음과 같은 메서드를 사용한다:
    • GET: 자원을 조회할 때 사용한다. 예를 들어, 특정 사용자의 정보를 가져올 때 사용된다.
    • POST: 새로운 자원을 생성할 때 사용한다. 예를 들어, 새로운 게시글을 작성할 때 사용된다.
    • PUT: 기존 자원을 수정할 때 사용된다. 전체 자원 정보를 업데이트할 때 주로 사용된다.
    • PATCH: 기존 자원의 일부를 수정할 때 사용된다. 필요한 부분만 변경할 수 있다.
    • DELETE: 자원을 삭제할 때 사용된다. 특정 사용자의 정보를 삭제할 때 사용된다.

CRUD 작업

CRUD는 데이터베이스에서 자원을 관리하는 기본적인 작업을 의미한다. 각각의 작업은 다음과 같다:

  1. Create (생성): 새로운 자원을 추가하는 작업이다. 예를 들어, 사용자가 회원가입을 하면 새로운 사용자 데이터가 생성된다. 이때는 POST 메서드를 사용한다.
  2. Read (조회): 기존 자원의 정보를 가져오는 작업이다. 예를 들어, 사용자가 자신의 프로필을 조회할 때 해당 사용자 정보를 서버에서 가져온다. 이때는 GET 메서드를 사용한다.
  3. Update (수정): 기존 자원의 정보를 변경하는 작업이다. 사용자가 자신의 프로필 정보를 수정할 때, 수정된 정보를 서버에 전송한다. 이때는 PUT 또는 PATCH 메서드를 사용한다.
  4. Delete (삭제): 기존 자원을 제거하는 작업이다. 예를 들어, 사용자가 게시글을 삭제할 때 해당 게시글이 서버에서 삭제된다. 이때는 DELETE 메서드를 사용한다.

Express.js로 REST API를 구현한 예시

CREATE

/** 할 일 등록 API */
router.post('/todos', async (req, res, next) => {
  // 1. 클라이언트로부터 받아온 value 데이터를 조회한다.
  try {
    const validation = await createdTodoSchema.validateAsync(req.body);
    const { value } = validation;

    // 1-5. 만약 클라이언트가 value 데이터를 전달하지 않았을 때, 클라이언트에게 에러 메시지를 전달한다.
    if (!value) {
      return res
        .status(400)
        .json({ errorMessage: '해야할 일(value) 데이터가 존재하지 않습니다.' });
    }

    // 2. 데이터베이스에서 order를 기준으로 마지막 데이터를 조회한다.
    // 이때 사용하는 findOne() 메서드는 한 개의 데이터를 조회할 수 있다.
    const todoMaxOrder = await Todo.findOne().sort('-order').exec();

    // 3. 만약 데이터가 하나라도 존재한다면 현재 해야 할 일의 order라는 변수에 (todoMaxOrder.order+1)을 할당하고, 반대로 존재하지 않다면 order를 1로 설정한다.
    const order = todoMaxOrder ? todoMaxOrder.order + 1 : 1;

    // 4. 해야 할 일 등록
    const todo = new Todo({ value, order });
    todo.save();

    // 5. 해야 할 일을 클라이언트에게 반환한다.
    return res.status(201).json({ todo });
  } catch (error) {
    // router 다음에 있는 에러 처리 미들웨어를 실행한다.
    next(error);
  }
});

READ

/** 할 일 목록 조회 API */
router.get('/todos', async (req, res, next) => {
  // 1. 해야할 일 목록 조회
  const todos = await Todo.find().sort('-order').exec();

  // 2. 해야할 일 목록 조회 결과를 클라이언트에게 반환
  return res.status(200).json({ todos });
});

UPDATE

/** 할 일 순서 변경, 완료, 내용 변경 API */
router.patch('/todos/:todoId', async (req, res) => {
  // 변경할 '해야할 일'의 ID 값을 가져옵니다.
  const { todoId } = req.params;
  // '해야할 일'을 몇번째 순서로 설정할 지 order 값을 가져옵니다.
  const { order, value, done } = req.body;

  // 순서를 변경하려는 '해야할 일'을 가져온다. 만약, 해당 ID값을 가진 '해야할 일'이 없다면 에러를 발생시킨다.
  const currentTodo = await Todo.findById(todoId).exec();
  if (!currentTodo) {
    return res
      .status(404)
      .json({ errorMessage: '존재하지 않는 해야할 일입니다.' });
  }

  // body로 입력받은 순서에 위치한 '해야할 일'을 찾는다.
  if (order) {
    const targetTodo = await Todo.findOne({ order }).exec();
    if (targetTodo) {
      targetTodo.order = currentTodo.order;
      await targetTodo.save();
    }
    currentTodo.order = order;
  }

  if (done !== undefined) {
    currentTodo.doneAt = done ? new Date() : null;
  }

  if (value) {
    currentTodo.value = value;
  }

  await currentTodo.save();

  return res.status(200).json({});
});

DELETE

/** 할 일 삭제 API */
router.delete('/todos/:todoId', async (req, res, next) => {
  const { todoId } = req.params;
  const todo = await Todo.findById(todoId).exec();

  if (!todo) {
    return res
      .status(404)
      .json({ errorMessage: '존재하지 않는 todo 데이터입니다.' });
  }

  await Todo.deleteOne({ _id: todoId });

  return res.status(200).json({});
});

export default router;

에러 처리 미들웨어

try catch문을 사용하면 에러 처리를 훨씬 편리하게 수행할 수 있다. 아래는 그 방식의 예시 중 하나다.

router.get('/accounts/characters/:accountId', async (req, res, next) => {
  try {
    const { accountId } = req.params;
    const account = await prisma.accounts.findUnique({
      where: { accountId: +accountId },
    });
    if (!account) {
      return res.status(404).json({ message: '존재하지 않는 계정ID입니다.' });
    }

    const characterList = await prisma.characters.findMany({
      where: { accountId: +accountId },
      select: {
        characterId: true,
        characterName: true,
        class: {
          select: {
            className: true,
          },
        },
      },
    });

    return res.status(200).json(characterList);
  } catch (err) {
    next(err); // 에러 발생 시 next()를 통해 error를 인자로 전달한다.
  }
});
export default (err, req, res, next) => { 
  // 미들웨어가 호출되는 과정에서 매개 변수로 error를 받게 된다면 이 함수가 실행된다.
  console.log('에러 처리 미들웨어가 실행되었습니다.');
  console.error(err);

  if (err.name === 'ValidationError') {
    return res.status(400).json({ errorMessage: err.message });
  }
  return res
    .status(500)
    .json({ errorMessage: '서버에서 문제가 발생했습니다.' });
};

'JS > TIL(Today I Learned)' 카테고리의 다른 글

2024-12-06  (2) 2024.12.06
2024-12-05  (0) 2024.12.05
2024-12-03  (1) 2024.12.03
2024-12-02  (0) 2024.12.02
2024-11-29  (1) 2024.11.29

댓글