[Node.js] 동기 HTTP & 비동기 HTTP

Yunhye Park·2023년 10월 20일
0
post-custom-banner

백엔드 수업에 들어선 요즘. 복습은 해도 정리를 안 해서인지 머릿속이 자꾸 꼬이는 기분이 들었다.

수업 자료로 쓰는 ppt는 키워드랑 예제 위주로 보고, 키워드를 꼬리에 꼬리를 물어가며 큰 그림을 이해해보는 게 이번 포스팅 정리 목표다.

0. 환경 세팅

  1. node.js 설치
  2. VSCode에서 디렉토리 만들고 터미널 열어서 위치 이동 : cd .\somewhere
  3. package.json 파일 생성 : npm init
  4. express와 ejs 설치 : npm i express ejs

ejs로 프론트를, express로 백을 담당한다. 수업 듣는 와중엔 이것도 어렴풋했는데 쌩뚱맞게도 HTTP 프로토콜을 복습하다가 알아차렸다. 👀

1. HTTP Protocol

HTTP 프로토콜을 그림으로 나타내면 다음과 같다.

그림으로 봐도 아리까리한 건 예제로 보는 게 좋다.

2. 동기 HTTP

ejs 형식으로 회원가입 폼 하나를 작성해준다. 유저의 아이디, 패스워드, 자신의 이름을 작성해 폼을 제출한다.

//index.ejs

// post 요청
<form action="/signup" method="post">
  <input type="text" name="id" placeholder="Enter your email">
  <input type="password" name="pw" placeholder="Enter your pw">
  <input type="text" name="name" placeholder="Enter your name">
  <button type="submit">회원가입</button>
</form>

2.1 클라이언트가 서버 측에 무언가를 요청

코드의 첫 줄을 보자. form에 두 가지 속성()이 정의되었다. HTML에서 속성은 태그 자체만으로 정보가 충분하지 못할 때 보강해주는 것이듯 클라이언트도 유저의 정보뿐 아니라 처리에 필요한 내용을 추가로 보내야 한다.

유저가 정보를 다 입력하고 회원가입 버튼을 누른 순간, 브라우저가 서버에 POST 요청(method)을 하는데, 이는 /signup(action)이라는 url에서 보낸 거다. 결국 응답을 성공적으로 받으면 저 url로 이동될 테다. POST 요청은 body에 정보(아이디, 패스워드, 이름)를 실어서 전송하기 때문에 길이 제한이 있는 경우보다 많은 양을 전달할 수 있다.

이제 서버 측을 보자.

2.2 서버는 요청을 받고 응답

서버는 POST 요청을 받아 body에 담긴 정보(req.body)를 꺼내어본다. 그 데이터들을 데이터베이스에 저장할 수도 있고, 유저에게 확인 메일을 보낼 수도, 그외 다른 작업도 처리 가능하다. 물론 제멋대로 하는 게 아니라 서버로 온 요청에 담겨있는 내용이어야 수행한다.

그나저나 GET 요청은 안 쓰고 POST로만 코드를 작성했다. 왜일까? 폼 제출은 전형적인 POST 요청이기 때문이다. 무슨 차이가 있는 모양인데.. 🤨

2.3 GET과 POST의 차이

2.3.1 POST 요청

  • body에 요청 받은 데이터를 담기 때문에 req.body
    • 파라미터(유저가 입력한 정보)가 url에 담기지 않는다.
    • 데이터 길이 제한이 없다.
    • 문자열, 숫자 등 여러 데이터 타입을 지원한다.
  • 브라우저에 기록이 남지 않는다.
  • 북마크로 추가할 수 없다.
  • Hardly Cached
  • 주로 리소스를 생성할 때 사용한다. 다른 말로 하자면, CRUD 중 C(create) 작업을 하고 싶을 때!
    • ex. 폼 제출, 리소스 생성, 파일 업로드, API 작업, 데이터 수정
  • Non-Idempotent*

POST 방식과 Non-Idempotent
이론적으로 생성, 수정, 삭제 모두 가능하긴 하지만, POST 요청은 요청할 때마다 데이터 연산 결과가 달라질 수(Non-idempotent) 있다.
그래서 POST 요청은 생성 시에, 열람은 GET, 수정엔 PUT이나 PATCH, 삭제는 DELETE 방식으로 대체하는 게 좋다.

2.3.2 GET 요청

정반대라고 생각하면 된다.

  • querystring*에 요청 받은 데이터를 담는다. req.query
    • 즉 파라미터(유저가 입력한 정보)가 url에 담긴다.
    • 데이터 길이 제한이 있다. 255자.
    • 문자열만 지원한다.
  • 브라우저에 기록이 남지 않는다.
  • 북마크로 추가할 수 없다.
  • Often Cached
  • 주로 리소스를 읽을 때 사용한다. 다른 말로 하자면, CRUD 중 R(Read) 작업을 하고 싶을 때!
  • Idempotent

    GET 요청과 Idempotent
    GET 요청은 여러 번 요청해도 언제나 동일한 응답을 돌려주기에(idempotent) 데이터가 절대 변하지 않는 상황, 즉 조회나 열람 시에 사용하도록 고안되었다.

💡 이처럼 GET은 데이터 열람용으로 설계되어서 수업에서 첫 화면 세팅할 때 다른 방식도 아닌 GET으로 요청을 보낸 것이었다!

// app.js

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

cf. form & input 주요 속성

✔️ form

  • action
    : 폼 전송할 서버 주소 지정. 외부 도메인을 서버로 둘 거면 도메인부터(ex. naver.com/~) 작성해야 한다.
  • method
    : 어떤 방식으로 요청할지 http 메서드 지정.

✔️ input

  • name
    : 프론트에서는 존재감이 미미했던 속성이지만, 백에서는 key로 동작하기 때문에 꼭 설정해줘야 데이터 식별이 가능하다.

그런데 폼을 제출했다는 건 버튼 타입이 submit이라는 말과 동일(<button type="submit">회원가입</button> ) 하다. 이렇게 되면 유저의 데이터를 입력 받던 폼이 사라지고, 완전히 새로운 페이지가 등장한다.

페이지의 특정 요소만 다시 렌더링해서 폼 형식을 그대로 유지할 순 없을까?

3. 비동기 HTTP

비동기(asynchronous)로 데이터를 처리하면 서버에 데이터를 보내는 동안 다른 작업도 처리할 수 있다.

예를 들어 지메일을 생각해보자. 새 메일이 오면 받은 메일함의 숫자가 변경되고, 메일 리스트 상단에 해당 메일이 추가된다. 메일을 클릭한다고 해서 완전히 새 페이지로 옮겨지는 게 아니라 여전히 사이드바는 남아있다. 이런 게 비동기 HTTP 방식이다. 서버와 클라이언트의 데이터 송수신이 dynamic하다고도 표현하는 듯하다.

서버에서 데이터를 받아올 때 XML 문법으로 작성해야 하는 건 참 번거롭다. 그렇게 3가지 대안이 나왔다.

Jquery Ajax, Axios, Fetch

마찬가지로 form을 예시로 볼 건데, 이번엔 로그인 상황을 가정해 연습 겸 GET과 POST 방식 모두 활용해본다. 앞서 살펴본 대로 GET 방식은 url에 유저가 기입한 정보가 전송되어서 실제 서버를 운영할 땐 지양하는 게 맞다.

// index.ejs

    <form name="login">
        ID <input type="text" name="id" />
        PW <input type="password" name="pw" />
        <div style="margin: 10px 0">
            <button type="button" onclick="처리방식함수()">로그인</button>
        </div>
    <h3 id="result"></h3>
    </form>

폼 제출이 아니라서 속성의 action과 method는 기입하지 않았다. button type도 그냥 button이다. 다만 클릭 이벤트를 달아주어 서버에 요청을 보내게 했다. 서버의 성공 여부는 h3 태그에 삽입하는 걸로 하겠다.

3.1 JQuery Ajax

전통적으로 사용된 ajax 방식을 제이쿼리로 작성하였다. 오래된 문법이라 Promise를 사용할 수 없다.

순서를 정리해보자면 이렇다.

  1. ejs로 html 작성
  2. jquery cdn 불러오기 (minified)
  3. script 작성 : GET이든 POST이든 type만 다를 뿐 나머지는 동일.
// index.ejs의 script

function ajaxGet(){
  const form = document.forms["login"]
  
      $.ajax({
		type: "get", // form 속성 중 method
        url: "/ajax", // form 속성 중 action
        data: {
           id: form.id.value,
           pw: form.pw.value,
           name: form.name.value
        },
		success: function(result) {
           $("#result").html(`{result.name}님 안녕하세요`)
          } 
     })
  }
  1. 요청 방식에 맞게 서버 측 응답 작성
// index.js

app.get('/ajax', function(req, res){
    res.send(req.query);
})

POST는 type: post로 변경해주면 된다.

Promise 기반이 아닌 걸 봤으니, 이젠 이를 기반으로 한 통신 방법을 보자.

3.2 Axios

promise를 기반한다는 건 then 메서드나 async/await을 사용해 간단하게 작성할 수 있다는 의미다. 브라우저 호환성도 좋다. 다만 외부 모듈이라서 npm i axios 하거나 cdn을 가져와야 한다. axios cdn

비동기 통신 말고도 다양한 기능을 제공하니 공식 문서를 참고해 보는 것도 좋겠다.

axios는 get과 post 방식에 따라 문법이 조금 다르다. 전자에서는 데이터를 params라는 ket로, 후자는 일전과 마찬가지로 data를 사용한다. 그래도 문법 차이만 있을 뿐 비동기 처리라는 목적은 같기에 큰 차이는 없다.

// index.ejs의 script

// axios GET 방식
function axiosGet(){
  const form = document.forms["login"]
  
      axios({
		method: "get", // method는 그대로
        url: "/ajax", // form 속성 중 action
        params: { // data는 params
           id: form.id.value,
           pw: form.pw.value,
           name: form.name.value
        }
     }).then((res)=>{
       const {name} = res.data // 객체 구조분해로 name만 가져옴
       $("#result").html(`${name}님 안녕하세요.`)
    })
  }

복습 겸 async/await 버전으로도 작성해보자.

async function axiosGet(){
  const form = document.forms["login"]
  
  const res = await axios({
		method: "get", // method는 그대로
        url: "/ajax", // form 속성 중 action
        params: { // data는 params
           id: form.id.value,
           pw: form.pw.value,
           name: form.name.value
        }
     })
  	const {name, id} = res.data
  	$('#result').html(`{name}({id})님 안녕하세요.`)
  }

POST 방식은 method: post, params가 아닌 data로 데이터를 작성
한다는 점만 다르고 나머지는 동일하다.

3.3 fetch

마찬가지로 promise 기반이다. JS 내장 모듈이라서 설치 없이 그냥 사용하면 된다. 그런데 최신 문법이라서 지원하지 않는 브라우저 혹은 브라우저 버전도 존재하고, JSON* 형태로 직접 변환하여 전달해야 하는 번거로움도 있다.

JSON?
JavaScript Object Notation. 자바스크립트 데이터를 문자열로 나타내기 위해 사용한다. 이름에서 보이듯 객체 형태를 띄고 있어서 읽기 편하고 익숙하다. 다만 key를 큰따옴표로 묶는다는 점이 다르다.
문자열, 숫자, 불리언, array, obejct 등 모든 데이터 타입을 소화한다.

모듈 설치 잘 되었나 확인했던 문서, package.json도 같은 형식이다.

그럼 코드를 작성해보자!
기본적인 데이터 작성 등은 위와 같고, 다른 점만 순서대로 정리해본다.

먼저 GET은 쿼리스트링으로 데이터를 주고받는데 fetch에서는 이를 직접 문자열로 변경하는 과정이 필요하다. 그리고 해당 문자열로 응답을 받을 때 json으로 변경도 해준다.

  1. querystring 생성
  2. 생성한 쿼리로 fetch 실행
  3. 실행한 것을 JSON으로 변경

차례로 async/await 사용, then과 async/await 사용, then 메서드 체이닝 버전으로 예제를 작성해봤다.

// fetch get 방식 [1] async/await 사용
async function fetchGet() {
  const form = document.forms["login"]
  const data = {
    id: form.id.value,
    pw: form.pw.value,
    name: form.name.value
  } 
  
  // 1. 쿼리 생성
  const urlQuery = new URLSearchParams(data).tostring()
  // 2. fetch 실행
  const oriRes = await fetch (`/fetch?${urlQuery}`)
  // 3. JSON으로 가공
  const res = await oriRes.json()
  console.log(res)
  
}

async/await 방식으로 작성하면 꽤 간결하다.

then 메서드 체이닝으로 작성하려면 콜백함수 파라미터로 res를 넘겨 res.json()을 return하고, 다시 그 결과를 res 파라미터로 받은 결과를 사용하면 된다.

말은 번거롭지만 과정을 이해하면 그렇게 복잡하진 않다.

// then 메서드 체이닝

fetch(`/fetch?${urlQuery}`)
.then((res)=>{
	return res.json()
}).then((res)=>{
	console.log(res)
})

참고로 then 메서드만 사용하면 이 함수에 async는 생략해도 무방하다.

마지막 콜백함수에 async/await를 사용해도 된다. await는 async 함수 내부에 있기만 하면 되니까.

// then과 async/await 사용

fetch(`/fetch?${urlQuery}`)
.then(async (res)=>{
	await console.log(res.json())
})

이어서 POST 방식이다. 여기서도 json으로 형식을 변환하는 과정을 거친다.

참고로 headers 객체 내 하이픈(-)으로 key를 설정해서 따옴표로 묶어주었다. 안 그러면 빼기 연산자로 인식한다.

fetch('/fetch', {
  const data = {
    id: form.id.value,
    pw: form.pw.value,
    name: form.name.value
  }
      
  method: 'post',
  headers: {
    'content-Type': 'application/json' // json으로 변경
  },
  body: JSON.stringfy(data) // json으로 변경
})

이후엔 then 메서드 체이닝, async/await 사용, then과 async/await 사용 중에서 마음에 드는 걸로 작성하면 된다.

💡 그래서 어떤 API가 좋을까?

여러 방법을 배웠으니 무엇을 언제 사용하는 게 가장 좋을지 특징들을 비교해 보고 싶어졌다. 일단 Promise 객체를 리턴하지 않는 ajax는 제외하고 브라우저 호환성, 보안성, 편리성 등을 기준으로 살펴보았다.

4.1 axios 특징

🟢 브라우저 호환성
axios 호환성

🟢 보안성
XSRF Protection

🟢 편리성
-request 취소 가능
-request 인터셉트 가능
-JSON 데이터 자동 변환

🔵 외부 모듈이라서 설치 필요 BUT 써드파티 패키지라서 방법 간단!
npm i axios

4.2 fetch 특징

🔵 브라우저 호환성
axios에 비해 상대적으로 최신 버전 지원 부족
fetch 호환성

❌ 보안성
XSRF Protection 없음

🔵 편리성
-request 취소와 타임아웃 없음 BUT AbortController 이용
-인터셉트 제공 안 함 BUT Global Fetch Method를 Overwrite 이용해 정의
-데이터 전송할 때마다 JSON 데이터 변환 필요

🟢 내장 모듈이라서 별도 설치 필요 없음

결론

axios는 지원 기능이 많은데 설치도 간편하니까 좋은 것 같다. 무엇보다 데이터 변환 과정 없는 axios를 접한 다음에 fetch에서 데이터 변환을 거쳐서인지 은근 번거롭다고 느꼈다. 하지만 fetch의 GET 요청은 async/await를 이용할 때 가장 간결해 보인다.

무엇이든 하나로 통일해 작성하는 건 당연하겠고, 앞에 열거한 특징을 고려했을 때 axios가 조금이나마 더 나은 선택지 같긴 하다.


참고

📍 Essential Guide to HTTP POST Request Method
📍 Handling an HTML Form – GET and POST Methods, and Data Encoding [Dev Concepts #38]
📍 Get과 Post의 차이를 아시나요?
📍 axios github
📍 fetch와 axios 차이점과 비교


덧붙이는 말

🔆 포스팅 작성에 필요한 개념들은 다 찾아보고, 최소한 나의 언어로 가공할 수 있을 만큼은 이해한 후에 적었다. 그래서 시간이 오래 걸렸는데(4시간......) 여전히 모르고 넘어간 개념들이 있다.

  • API operation
  • cache
  • XSRF Protection

포스팅의 흐름과는 상대적으로 거리가 멀어서 자세히 알아보지 않았던 거라 이후에 찾아보기로!

🔆 뭐든 검색이야 손쉽게 할 수 있다만 이해하지 못한 채 복붙하거나 단어만 깔짝깔짝 바꾸면 나에게 남는 게 없다. 막 개발 블로그 시작했을 때 그랬던 것 같다. 계속 회고 작성 방향에 대해 고민했는데 짐이 좀 덜어진 느낌이다.

🔆 마찬가지로 이해 없이 코드를 무작정 따라 치면 아무것도 남는 게 없다. 완벽히 스스로 답을 찾진 못했어도 생각과 고민 끝에 얻은 건 흔적이라도 남는다.

수업 시간 끝나고 복습했을 땐 내가 얼마나 이해가 된 건지 몰랐는데 이렇게 쭉 정리해 보니 어느 정도 습득하긴 했구나 싶다. 특히 promise! 어려운 개념 같았는데 코드를 직접 짤 수 있단 게 신기했다.

🔆 리액트 강의를 따로 결제해서 수강 중인데 강사 분의 티칭 방식이 도움된 듯하다. 머리를 부여잡고 스스로 깨작거리고, 깨작임 끝에 답을 깨닫고, 놀랍고 신기한 마음으로 정리하고, 다음 강의를 들으며 또 깨작이고.. 커다란 걸 성취하는 것보다 이렇게 작게작게 쌓아가는 즐거움이 무진장 크다.

물론 수업에서도 마찬가지다. 나는 setTimeout 비동기처리로 1초마다 div 색깔이 바뀌는 코드를 추상화했을 때의 짜릿함을 잊지 못해... 심지어 색깔이 극적으로 짠짠짠 바뀌어서 내적 쾌감이 대단했다. 내가 기억하는 첫번째 깨작임의 성공.

🔆 오늘도 어제보다 더 나은, 더 아는 내가 되게끔 공부하자! 👏

profile
일단 해보는 편
post-custom-banner

0개의 댓글