프론트엔드와 백엔드 사이

원민관·2026년 5월 28일

[TIL]

목록 보기
210/210
post-thumbnail

1. Status Code ✍️

1-1. 프론트엔드와 상태 코드 🎯

HTTP 상태 코드는 프론트엔드에서 백엔드로 보냈던 요청의 수행 결과를 의미하는 일종의 약속이며, API를 구성하는 핵심 요소 중 하나입니다. 상태 코드와 관련하여, 백엔드는 잘 모르는 프론트엔드의 슬픈 사정이 있습니다.

아래는 요청이 실패했음에도, 백엔드에서 상태 코드로 요청 성공을 의미하는 200 OK를 내려준 상황입니다.

GET /api/users/123
HTTP/1.1 200 OK
{ "success": false }

프론트엔드에서 사용하는 대부분의 HTTP 통신 라이브러리나 API는 백엔드에서 내려주는 상태 코드에 따라 성공/실패 여부를 판단하고, 요청이 실패한 경우에만 에러를 던지게 됩니다. 일반적인 경우 프론트엔드 통신 코드는 다음과 같은 형태로 작성됩니다.

async function fetchUsers () {
  try {
    const response = await fetch('/api/users/123');
    return response.json();
  }
  catch (e) {
    alert('요청이 실패했어요!');
  }
}

그런데 백엔드에서 요청이 실패했음에도 불구하고 상태 코드로 200번대 코드를 내려준다면 프론트엔드 통신 코드는 다음과 같이 수정됩니다.

async function fetchUsers () {
  try {
    const response = await fetch('/api/users/123');
    const { success } = await response.json();
    if (!success) {
      throw new Error();
    }
  } catch (e) {
    alert('요청이 실패했어요');
  }
}

불필요한 예외 처리가 추가되어 코드의 가독성을 해칩니다. 그렇다고 서버가 보내주는 에러를 무시하고 핸들링을 하지 않을 수도 없는 노릇입니다.

이제 상태 코드를 정확하게 인지하고 사용하기 위해 대표적인 상태 코드들을 살펴보도록 하겠습니다.

1-2. 200번대 🎯

200번대 상태 코드는, 프론트엔드에서 요청한 작업을 백엔드가 성공적으로 수행했다는 상태라는 것을 알려주는 코드입니다.

200 OK ✅
상태 코드 200은 단순히 작업이 성공했음을 의미합니다. 대부분의 프론트엔드 개발자들은 자신이 요청한 작업이 무엇인지 정확히 인지하고 있기 때문에, 서버로부터 요청에 대한 작업이 성공적으로 처리되었다는 사실만 전달받으면 됩니다. 200 상태 코드로 API 성공 상태를 갈음하는 경우가 많다는 것이죠.

201 Created ✅
상태 코드 201은 요청이 정상적으로 수행되어 리소스가 새롭게 생성되었다는 것을 의미합니다. 회원 가입을 요청하거나 게시글을 포스팅하는 상황처럼, 데이터베이스에 새로운 로우가 추가된 경우에는 201 상태 코드를 적용하여, 상태 코드 200과 구분해 주는 것이 더 좋겠습니다.

204 No Content ✅
상태 코드 204는 요청이 정상적으로 수행되어 리소스가 깔끔하게 제거되었다는 것을 의미합니다. 여기서 중요한 것은 삭제 작업이 Soft Delete 인지, Hard Delete 인지는 아무런 상관이 없다는 것입니다. 프론트엔드에서 알아야 할 정보는 오직 "해당 리소스는 삭제되었고, 더는 사용할 수 없다"라는 점입니다.

1-3. 300번대 🎯

300번대 상태 코드는, 리다이렉션에 관련된 상태를 알려주는 코드입니다.

301 Moved Permanently ✅
상태 코드 301은 리다이렉션을 위한 코드 중 가장 많이 사용되는 코드입니다. 브라우저는 응답으로 301 코드를 받으면, HTTP 헤더에 들어있는 Location 필드를 찾아보고, 해당 필드에 담긴 URL로 자동으로 리다이렉션합니다.

HTTP/1.1 301 Moved Permanently
Location: https://minkwan.com/user/1234

아래와 같이 80 포트로 접속한 사용자를 Nginx에서 443 포트로 리다이렉트하는 경우에도 301 코드를 적용하여 처리할 수 있습니다.

server {
    listen         80;
    server_name    minkwan.com;
    return         301 https://$host$request_uri;
}
server {
    listen         443 ssl;
    server_name    minkwan.com;
    ...
}

304 Not Modified ✅
상태 코드 304는 프론트엔드에서 요청한 리소스가 이전 요청 때와 전혀 달라진 점이 없음을 표현하는 코드입니다. 그렇다면 백엔드는 굳이 프론트엔드로 리소스를 전송해야 할 이유가 없겠죠. 프론트엔드에서는 캐싱 해놓은 리소스를 사용함으로써 불필요한 통신 과정을 줄일 수 있습니다. 프론트엔드 입장에서는 서버로부터 요청된 데이터를 받은 것이 아니라 캐싱 해놓았던 리소스를 사용하는 것이므로 캐싱 된 리소스로 리다이렉션 되었다고 간주하는 것입니다. 이러한 이유로 상태 코드 304를 암묵적인 리다이렉션이라고 칭하기도 합니다.

1-4. 400번대 🎯

400번대 상태 코드는, 서버에게 보낸 요청이 잘못된 경우를 의미합니다. 높은 확률로 프론트엔드 개발자가 잘못한 상황이라고 이해할 수 있습니다.

400 Bad Request ✅
상태 코드 400은 밑도 끝도 없이 프론트엔드에서 요청을 잘못 보낸 상황을 나타냅니다.

401 Unauthorized ✅
상태 코드 401은 인증되지 않은 사용자가 인증이 필요한 리소스를 요청하는 경우를 나타내는 코드입니다. 같은 말이지만, 로그인이 필요한 API를 비로그인 사용자가 호출한 경우에 많이 사용됩니다.

403 Forbidden ✅
상태 코드 403은 프론트엔드에서 접근이 금지된 리소스로 요청을 보낸 상황을 나타내는 코드입니다. 상태 코드 401과의 차이는, 403은 상대의 신원을 궁금해하지 않습니다. 인증과 상관없이, 해당 리소스를 요청하는 것은 무조건 금지라고 나타내는 것입니다. 가령, HTTPS 프로토콜로만 접근해야 하는 리소스에 HTTP로 접근한 경우, 서버에서 403 응답을 내리기도 합니다.

404 Not Found ✅
상태 코드 404는 말 그대로 요청한 리소스가 존재하지 않는다는 것을 의미합니다.

405 Method Not Allowed ✅
상태 코드 405는 현재 리소스에 맞지 않는 메소드를 사용한 상황을 의미합니다. 백엔드 프레임워크의 경우, 특정 컨트롤러에 해당 메소드를 사용하는 로직이 없다면 자동으로 405를 내려주기도 합니다.

406 No Acceptable ✅
상태 코드 406은 알맞은 컨텐츠 타입이 없을 때를 표현하는 코드입니다. 백엔드로 리소스를 요청할 때 HTTP 헤더의 Accept 필드를 사용하여 text/html 타입의 리소스를 요청했는데, 해당하는 리소스가 없다면 406 상태를 내려주게 됩니다.

408 Request Timeout ✅
상태 코드 408은 프론트엔드와 백엔드의 연결은 성사되었지만, 요청 본문이 지속적으로 서버에 도착하지 않는 상황을 의미합니다. HTTP 프로토콜을 사용하여 통신을 할 때에는 반드시 프론트엔드와 백엔드 간의 연결을 생성한 후 요청 본문에 해당하는 데이터를 전송하게 되는데, 이 과정에서 연결은 제대로 생성되었지만 백엔드에서 아무리 기다려도 클라이언트가 보낸 요청 본문을 받지 못하는 경우에 발생하게 됩니다.

429 Too Many Requests ✅
상태 코드 429는 프론트엔드에서 백엔드로 너무 많은 요청을 보낸 상황을 나타내는 코드입니다. 요청을 많이 보낸 경우도 해당하지만, 유료 API를 사용하는 경우에 현재 금액으로 사용할 수 있는 API 요청 횟수를 초과해서 "돈을 더 내세요"라는 의미로 사용되는 경우도 많습니다.

1-5. 500번대 🎯

500번대 상태 코드는, 백엔드에서 오류가 발생한 경우를 의미합니다. 높은 확률로 백엔드 개발자가 잘못한 상황이라고 이해할 수 있습니다.

500 Internal Server Error ✅
상태 코드 500은 백엔드 내에서 알 수 없는 에러가 발생한 경우를 나타내는 코드입니다. 제대로 핸들링되지 않은 에러가 발생한 경우가 많습니다. 더욱이, 핸들링되지 않은 에러의 원인을 프론트엔드에 고스란히 내려주는 것은 보안 사고가 발생할 가능성이 매우 높아서, 500 상태 코드로 에러의 발생 자체만을 알려주는 경우가 대부분입니다.

502 Bad Gateway ✅
상태 코드 502는 백엔드 어플리케이션이 아예 멈춘 상황을 나타내는 코드입니다. 단, 502는 중간 서버가 내리는 상태 코드입니다.

server {
    listen 80;
    server_name minkwan.com;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

프록시 서버와 백엔드 어플리케이션 간 연결된 추상적인 통로를 게이트웨이라고 부릅니다. 백엔드 어플리케이션이 멈춘 경우 프록시 서버가 대신해서 프론트엔드로 502 Bad Gateway라고 응답을 보내는 것입니다.

503 Service Unavailable ✅
상태 코드 503은 502보다는 일시적인 상황을 의미하는 상태 코드입니다. 일반적으로 서버에 부하가 심해서 현재의 요청을 핸들링 할 수 있는 여유가 없는 경우에 많이 사용됩니다.

504 Gateway Timeout ✅
상태 코드 504는 408과 마찬가지로 요청에 대한 타임아웃을 의미합니다. 단, 타임아웃이 프론트엔드에서 보낸 요청 때문에 발생하는 것이 아니라, 백엔드 아키텍처 내부에서 서버끼리 주고받는 요청에서 발생합니다. Nginx에서 백엔드로 프론트엔드의 요청을 전달했는데, 백엔드 어플리케이션이 일정 시간 동안 응답을 하지 않은 경우, Nginx는 프론트엔드로 504 상태 코드를 내려주게 됩니다.

2. REST API / RESTful API ✍️

2-1. REST API 🎯

REST는 REpresentational State Transfer를 의미합니다. 한국어로는 '표현된 상태'라고 이해할 수 있습니다. 즉, 통신을 통해 데이터를 주고받는 것이 아니라 '상태'를 주고받는 것입니다.

GET https://iamserver.com/api/users/2
Host: iamserver.com
Accept: application/json

백엔드에게 "2번 유저의 상태를 JSON으로 표현해 줘"라고 요청을 보낸 것입니다. 즉, 프론트엔드는 2번 유저의 리소스를 받은 것이 아니라 JSON으로 표현된 2번 유저 리소스의 현재 상태를 받은 것입니다. 계좌에 찍힌 숫자가 실제 돈을 표현한 것과 같은 이치입니다.

2-2. RESTful API 🎯

REST API를 특정 규칙에 맞게 잘 구성한 API를 RESTful 하다고 이해할 수 있습니다. 다만 이번 포스팅에서는 RESTful API 자체보다는, 메소드와 멱등성에 대해 이야기하고자 합니다.

멱등성 ✅
멱등성(Idempotency)은 수학이나 컴퓨터 과학에서 동일한 연산을 여러 번 적용해도 결과가 달라지지 않는 성질을 의미합니다. 한 번 실행하든, 여러 번 연속해서 실행하든 항상 최초의 결과와 똑같은 상태를 유지합니다.

GET ✅
GET은 리소스를 조회하는 메소드입니다. GET은 단지 리소스를 읽어오는 행위를 의미하기에, 아무리 여러 번 수행해도 기존 데이터가 변경되지는 않습니다. 즉, GET은 멱등성이 보장되는 메소드입니다.

PUT ✅
PUT은 리소스를 대체하는 메소드입니다. 요청에 담긴 리소스로 기존 리소스를 그대로 대체하기 때문에, 여러 번 수행해도 연산 결과가 동일할 것입니다. 즉, PUT 역시 멱등성이 보장되는 메소드입니다.

DELETE ✅
DELETE는 리소스를 삭제하는 메소드입니다. 삭제 역시 여러 번 수행해도 연산 결과는 동일합니다. DELETE도 멱등성이 보장되는 메소드입니다.

POST ✅
POST는 리소스를 생성하는 메소드입니다. POST는 여러 번 수행하면 리소스가 매번 새롭게 생성됩니다. 멱등성이 보장되지 않는 메서드입니다. POST 메소드와 같이 멱등성을 보장하지 않는 동작은 한 번 수행될 때마다 어플리케이션의 상황을 전혀 다르게 변화시킬 수도 있습니다.

PATCH ✅
PATCH는 리소스의 일부를 수정하는 메소드입니다. 단순히 필드의 일부를 수정하는 경우라면 PATCH도 멱등성이 보장된다고 볼 수 있습니다. 다만, PATCH 메소드를 처음으로 정의해놓은 RFC-5789 문서를 보면, 별다른 제약조건이 명시되어 있지 않습니다.

PATCH users/1
{
  $increase: 'age',
  value: 1,
}

리소스의 일부를 위와 같이 동적으로 수정하는 상황이라면, 동작을 수행할 때마다 리소스가 계속해서 변화합니다. 스펙 상의 구현 방법에 대한 제약이 없으니, 구현 방식에 따라 멱등성을 보장할 수도 아닐 수도 있는 것입니다.

HTTP 메소드는 특정 자원에 대한 CRUD 작업이기 때문에, 멱등성에 대한 이해가 있어야 어플리케이션이 예상하지 못한 방향으로 동작하는 것을 방지할 수 있습니다.

3. 멱등성: 결제 취소 예제로 이해하기 ✍️

그런데 REST API라는 API 설계 표준이 왜 필요할까요? 상태 코드는 왜 필요할까요? 결국 데이터를 안전하게 주고받기 위한 노력의 결과 아닐까요? 그렇다면 무엇이 데이터를 "안전하지 않게" 할까요? 답은 멱등성에 있습니다.

사용자가 결제하는 시점에 네트워크 오류나 타임아웃으로 인해 결과를 받지 못하는 상황을 생각해 보죠. 멱등성이 보장된 결제 API를 사용하면, 다시 같은 요청을 보내지 않고도 전에 받지 못한 결과만 다시 받을 수 있을 때 편리할 것입니다. 추가적으로, 중복 요청이 되더라도(일명 '따닥') 실제로는 결제가 되지 않아서 안심하고 여러 번 요청할 수도 있습니다.

3-1. 클라이언트 코드 🎯

let idempotentKey = generateUUIDv4()

function async cancelPayment(idempotencyKey: string) {
  try {
    return await axios.post("https://myshop/cancel-payment",
      {
        orderId: UINQUE_ORDER_ID
        amount: 100,
      },
      {
        headers: {
          "Idempotency-Key": idempotentKey // 헤더에 멱등키를 추가합니다.
        }
      }
    )
  } catch(e) {
    if (e.name === "TIMEOUT") { // 타임아웃이 일어났을 때 같은 요청을 보낼 수 있습니다.
        return await cancelPayment(idempotencyKey)
    }
    console.error("ERROR")
  }
}

const response = await cancelPayment(idempotentKey);

클라이언트는 요청을 보낼 때 UUID v4와 같은 무작위 한 멱등키를 발급해서 헤더에 실어 보냅니다. 만약 서버의 응답이 너무 늦어져서 타임아웃 에러가 발생하면, 방금 전과 '동일한 멱등키'를 그대로 유지한 채 다시 요청을 보냅니다.

3-2. 서버 코드 🎯

let cancelReq = {
  orderId: req.body.orderId
  amount: req.body.amount,
};

let idempotencyKey = req.headers.idempotencyKey || null // 요청 헤더에서 멱등키를 가져옵니다.

// 멱등키가 있고 멱등 응답도 저장되어 있다면 실제 처리하지 않고 저장된 응답을 내보냅니다.
if (idempotencyKey != null && idempotencyResponses.has(idempotencyKey)) {
  const response = idempotencyResponses.get(idempotencyKey);
  return res.status(response.status).json(response);
};

const result = cancelProcessor.cancel(cancelReq); // 실제로 취소를 처리합니다.

// 멱등키가 있으면 멱등응답을 저장합니다.
if (idempotencyKey != null) {
  idempotencyResponses.set(idempotencyKey, result);
}

const responseBody = {
  message: `결제 취소 성공`,
};

return res.status(200).json(responseBody);

시나리오 A: 멱등키가 처음으로 들어왔을 때 ✅
멱등키가 처음으로 들어왔으니 가장 처음에 마주치는 조건문을 넘어가고 실제 취소를 진행합니다. 결제 취소가 성공하면 다음번에 동일한 요청이 올 것을 대비해서 멱등키와 처리 결과를 저장합니다.

시나리오 B: 이미 처리했던 멱등키일 때 ✅
네트워크 오류 등으로 클라이언트가 동일한 멱등키로 요청을 다시 보낸 상황입니다. 가장 처음에 마주치는 조건문 내부의 로직을 실행하게 됩니다. 멱등키가 있고 멱등 응답도 저장되어 있기 때문에 저장된 응답을 그대로 클라이언트에게 반환합니다.


References

[1] Evan Moon, "서버의 상태를 알려주는 HTTP 상태 코드"
https://evan-moon.github.io/2020/03/15/about-http-status-code/
[2] Evan Moon, "프론트엔드와 백엔드가 소통하는 엔드포인트, RESTful API"
https://evan-moon.github.io/2020/04/07/about-restful-api/
[3] 토스페이먼츠 개발자센터 블로그(한주연), "멱등성이 뭔가요?"
https://docs.tosspayments.com/blog/what-is-idempotency

profile
Write a little every day, without hope, without despair ✍️

0개의 댓글