[에러] Cannot set headers after they are sent to the client

sarifor·2022년 4월 13일
0

문제

  • Node.js + Express 앱에서 axios로 웹페이지의 데이터를 긁어올 때, 아래와 같은 에러가 뜸.
    (GitHub에서 코드 보기)

환경

Node.js 14.19.1
npm 6.14.16

원인

  • 데이터를 긁어온 후에도 코드가 종료되지 않아서 HTTP Stateless 원칙이 깨져버림. 1 Request에 2 Response가 되어버렸던 것.
  • 때문에 Response 처리 한쪽의 코드를 종료시켜 다음 Response 처리 코드가 문제 없이 실행되게 해줄 필요가 있었다.

해결

  • Response 받아오는 처리 두 개 중 하나를 별도의 함수화하여 코드를 종료시킴.
    (GitHub에서 코드 보기)
  • 에러가 If 조건문에서 발생했다면, return 명령문을 붙여서 해결할 수도 있다. 마찬가지로 코드가 종료된다. (미검증)

    app.post('/test', (req, res) => {
     
     if (!req.body.name) {
       return res.status(400).json({ // return을 붙이면 Response가 클라이언트에 전송되고 코드가 종료된다.
         status: 'error',
         error: 'req body cannot be empty',
       });
     }
    
     res.status(200).json({
       status: 'succes',
       data: req.body,
     })
     
    });

보충

위 이슈의 원인과 해결책을 찾다가 알게 된 지식에 대하여 정리해 보았다. 주로 JavaScript, HTTP 관련이다.

1. JavaScript 동작 원리

Overview

엔진 구조

  • JavaScript 엔진(V8 엔진)은 Memory HeapCallStack으로 구성되어 있다.
  • Memory Heap에는 참조 타입 값을 저장하고(상세: 본문 아래쪽), CallStack에는 함수를 순차적으로 넣어 실행시킨다.

일반 처리 (동기 처리)

  • 함수가 실행되면 CallStack에 순차적으로 들어가고, 종료되면 빠진다.
  • JavaScript의 CallStack은 1개이다. (Single Thread)

CallBack 함수 처리 (비동기 처리)

  • 오래 걸리는 일은 Web API로 처리한다.
  • Web API의 Event가 실행된 후 처리될 CallBack 함수는 실행 시점이 되면 CallBack Queue에 저장된다. 아래는 예시.
    setTimeout(
      function() { // 이 부분이 CallBack 함수
        console.log("Hungry"); 
      }, 0);
  • Event Loop는 CallStack이 비어 있는지 주기적으로 확인하여, CallBack Queue에서 CallBack 함수를 CallStack에 옮겨다 준다.
  • Web API, CallBack Queue, Event Loop는 브라우저Node.js에서 제공하는 것이다. (Multi Thread)
  • async & await으로 비동기 처리를 동기 처리처럼 쓸 수 있다.

2. JavaScript 변수/값의 메모리 저장 원리

  • 모든 변수(원시 타입, 참조 타입)은 CallStack의 실행컨텍스트 렉시컬 환경(후일 보충)에 { name: value }로 저장된다.
  • 원시 타입 값은 CallStack에 저장된다.
  • 참조 타입 값은 Heap 영역에 저장된다.
    • 참조 타입 변수는 CallStack 주소를 읽고, 그 CallStack 주소는 Heap 영역 주소를 읽는 것.
  • 데이터 변경 유무는 CallStack 값을 기준으로 판단한다.
    • 때문에 const로 선언된 참조 타입의 경우, 값을 변경할 수 있다. (상세: 아래쪽)

3. let & const 쓰임새 및 비교

  • 원시 타입 변수이든 참조 타입 변수이든, 처음엔 CallStack 주소를 읽는다.
  • 변수의 값을 바꾼다는 것은, 사실 CallStack 주소값을 바꾼다는 것이다.
  • let은 CallStack(메모리) 주소를 변경할 수 있고, const는 변경할 수 없다.
  • 따라서 CallStack에 값을 저장하는 원시 타입의 경우, const로 선언되면 값을 변경할 수 없다.
  • 그러나 참조 타입 변수의 경우, const로 선언되어도 값 변경이 가능하다. CallStack의 주소값은 유지되기 때문이다.
  • 정리하면, let이 붙은 변수 값은 원시 타입이든 참조 타입이든 수정이 가능하고, const가 붙은 변수 값은 원시 타입은 수정할 수 없으나 참조 타입은 수정이 가능하다.

4. JavaScript 함수의 return 명령문 특징

  • 함수 본문에서 return 명령문에 도달하면, 함수의 실행은 그 지점에서 중단된다.
  • 값을 제공한 경우, 함수를 호출한 곳에 그 값을 반환한다.
  • 함수 본문에 return 명령문이 없으면, return undefined 하는 것과 같다.
    • undefined란? (후일 보충)

5. HTTP는 Stateless

  • HTTP Protocol은 현재 상태를 저장하지 않는다. (Stateless)
  • Request 1개에 Response 1개를 보낼 뿐이다.
  • 단, CookieSession으로 Stateless를 극복할 수 있다.

요약

문제/원인/해결

  • Node.js + Express 앱에서 axios로 웹페이지를 긁어올 때 발생한 Cannot set headers after they are sent to the client 에러는 1 Request에 2 Response가 된 것이 원인이었고, Response 처리 한쪽의 코드를 별도의 함수로 빼서 종료시킴으로 해결.

보충

  • 1 Request 2 Response는 불가능하다. HTTP는 Stateless이기 때문이다.

  • 일반 함수는 CallStack에서 바로 실행되고, CallBack 함수는 일단 CallBack Queue에 넣었다가 나중에 CallStack으로 옮겨 처리한다.

  • 원시 타입 값은 CallStack에, 참조 타입 값은 Heap 영역에 저장된다.

  • 데이터 변경 유무는 CallStack 값을 기준으로 판단한다. 때문에 const로 선언된 참조 타입의 경우, 값을 변경할 수 있다.

  • 함수에서 return 명령문을 쓰면 코드가 종료된다.

  • 동기 vs 비동기

    구분처리 방식지원연관 키워드
    동기될 때까지 기다렸다가 처리JS 엔진Single Thread
    비동기되는 것부터 처리브라우저, Node.jsCallBack, async & await, Multi Thread

참고

profile
잠수 탄 블로그 같지만 살아있어요

0개의 댓글