백엔드/express.js

express 프레임워크 기본이론

syleemomo 2021. 10. 12. 21:18
728x90

 

express 라우팅 참고문서

 

Express 라우팅

라우팅 라우팅은 애플리케이션 엔드 포인트(URI)의 정의, 그리고 URI가 클라이언트 요청에 응답하는 방식을 말합니다. 라우팅에 대한 소개는 기본 라우팅을 참조하십시오. 다음 코드는 매우 기본

expressjs.com

 

기본 라우팅 및 API 테스트 

사용자에 대한 정보를 조회, 생성, 변경, 삭제하는 예제를 살펴보자!

app.get("/users", (req, res) => {
  // 데이터베이스에서 사용자 전체목록 조회
  res.send("all user list !")
})

GET 방식으로 /users URL 에 요청을 보내면 데이터베이스에서 사용자 전체 목록을 조회하고 그 결과를 응답으로 전달한다. 

 

주의사항 :  body-parser 미들웨어가 설정되어 있어야 아래 예제들이 제대로 동작한다.

app.use(express.json());

body-parser 는 서버로 전달된 요청본문(request body)를 파싱한다. 해당 코드는 파싱후 req.body 에 파싱한 요청본문을 객체 형태로 추가한다. 

app.post("/users", (req, res) => {
  console.log(req.body.newUser)
  // 데이터베이스에 새로운 사용자 생성
  res.json(`new user - ${req.body.newUser.name} created !`)
})

POST 방식으로 /users URL 에 요청을 보내면 요청본문(request body)을 이용하여 데이터베이스에 새로운 사용자를 생성한다.

app.put("/users/:id", (req, res) => {
  console.log(req.body.updatedUserInfo)
  // 데이터베이스에서 id 에 해당하는 사용자 정보 조회 후 업데이트
  res.send(
    `user ${req.params.id} updated with payload ${JSON.stringify(
      req.body.updatedUserInfo
    )}!`
  )
})

PUT 방식으로 /users/1234 와 같은 URL 에 요청을 보내면 데이터베이스에서 id에 해당하는 사용자를 조회한 후 요청본문(request body)을 이용하여 사용자 정보를 변경한다. 요청본문은 페이로드(payload)라고도 한다. 

app.delete("/users/:id", (req, res) => {
  // 데이터베이스에서 id 에 해당하는 사용자 조회 후 제거
  res.send(`user ${req.params.id} removed !`)
})

DELETE 방식으로 /users/1234 와 같은 URL 에 요청을 보내면 데이터베이스에서 id에 해당하는 사용자를 조회한 후 삭제한다.

const express = require('express')
const app = express() 
const router = express.Router()
const port = 3000

app.use(express.json());  

app.get("/users", (req, res) => {
    // 데이터베이스에서 사용자 전체목록 조회
    res.send("all user list !")
})
app.post("/users", (req, res) => {
    console.log(req.body)
    // 데이터베이스에 새로운 사용자 생성
    res.json(`new user - ${req.body.newUser.name} created !`)
})
app.put("/users/:id", (req, res) => {
    console.log(req.body.updatedUserInfo)
    // 데이터베이스에서 id 에 해당하는 사용자 정보 조회 후 업데이트
    res.send(
      `user ${req.params.id} updated with payload ${JSON.stringify(
        req.body.updatedUserInfo
      )}!`
    )
})
app.delete("/users/:id", (req, res) => {
    // 데이터베이스에서 id 에 해당하는 사용자 조회 후 제거
    res.send(`user ${req.params.id} removed !`)
})

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

app.js 서버의 전체코드는 위와 같다. 

 

위의 4가지 라우팅에서 GET 방식은 브라우저 화면에서 직접 테스트할 수 있지만 나머지 방식은  API 테스트 도구가 필요하다.

API 테스트 도구

 

Talend API Tester - Free Edition

Visually interact with REST, SOAP and HTTP APIs.

chrome.google.com

POST 방식 API 테스트
PUT 방식 API 테스트
DELETE 방식 API 테스트

 

 

URL 대소문자 구분

/about
/About
/about/	
/about?foo=bar
/about/?foo=bar

express 프레임워크는 기본적으로 URL 대소문자를 구분하지 못한다. 사용자가 위와 같은 URL을 입력하면 express 프레임워크는 동일한 URL로 간주하고 아래와 같은 동일한 로직을 처리한다. 

app.get('/about', (req, res) => {
    res.send('this is about page!')
})

위에 나열한 모든 URL 을 브라우저에서 입력하고 서버로 요청을 보내면 express 프레임워크는 바로 위의 '/about' 을 처리하는 로직으로 동일하게 처리한다.  만약 정확히 사용자가 정확히 /about URL을 입력한 경우만 위 로직이 처리되고, 대소문자를 구분해서 위에 나열한 URL 목록을 다르게 처리하고 싶으면 아래와 같이 서버 설정을 해주면 된다. 

app.set('case sensitive routing', true);

이렇게 하면 사용자가 브라우저에서 /about 이라는 URL 주소를 정확히 입력한 경우만 해당 로직을 처리하고, /About /ABOUT /aboUT 등은 모두 해당 로직을 처리하지 않고,  페이지를 찾을수 없다는 등의 폴백 핸들러(오류 처리 로직)가 대신 처리한다. 하지만 실제로 해보면 아래와 같이 제대로 동작할때도 있지만, 제대로 동작하지 않는 경우도 많다. 

case sensitive 적용

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

app.set('case sensitive routing', true);
app.use(express.json());  

app.get('/about', (req, res) => {
    res.send('this is about page!')
})

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

app.js 서버의 전체코드는 위와 같다. 

 

URL 와일드카드

app.get("/users*", (req, res) => {
  res.send("users wildcards !")
})
app.get("/users/contact", (req, res) => {
  res.send("contact page !")
})
app.get("/users/city", (req, res) => {
  res.send("city page !")
})

get 메서드의 첫번째 인자는 라우팅 URL이다. * 는 와일드카드(wildcards)라는 것인데 /users* 는 사용자가 /users 로 시작하는 어떠한 URL로 사용자 요청을 서버로 전송해도 서버는 첫번째 로직만 실행한다. 즉, /users/contact /users/city URL 에 해당하는 로직은 절대 실행되지 않는다. 아래와 같은 URL로 실험을 해보자!

/usersname
/usersage
/users?card=3
/users/contact
/users/city

/users 로 시작하는 어떠한 URL도 와일드카드(*) 때문에 모두 첫번째 로직만 실행이 된다. 

app.get("/users/contact", (req, res) => {
    res.send("contact page !")
})
app.get("/users/city", (req, res) => {
    res.send("city page !")
})
app.get("/users*", (req, res) => {
    res.send("users wildcards !")
})

/users/contact 과 /users/city 경로에 대한 라우트 핸들러가 동작하기 위해서는 미들웨어 순서를 변경해주면 된다. 위와 같이 순서를 변경해주면, /users/contact/ 과 /users/city 경로가 아니며, /users 로 시작하는 모든 경로는 와일드카드 로직이 실행된다. 

 

URL 패턴매칭

정규표현식 참고자료

 

정규 표현식 - JavaScript | MDN

정규 표현식은 문자열에 나타는 특정 문자 조합과 대응시키기 위해 사용되는 패턴입니다. 자바스크립트에서, 정규 표현식 또한 객체입니다.  이 패턴들은 RegExp의 exec 메소드와 test 메소드  ,

developer.mozilla.org

정규 표현식의 특수문자는 종류가 다양하다. 모든 특수문자는 참고자료에 잘 정리되어 있다.  가령, 'user(name)?' 과 같이 문자열로 구성된 URL 내부에서 특수문자를 사용할 수 있다. 또한, URL이 문자열이 아닌 순수 정규 표현식으로도 URL 패턴매칭이 가능하다. 

정규표현식을 사용한 라우트 경로의 예시 참고 (라우트 경로 파트)

 

Express 라우팅

라우팅 라우팅은 애플리케이션 엔드 포인트(URI)의 정의, 그리고 URI가 클라이언트 요청에 응답하는 방식을 말합니다. 라우팅에 대한 소개는 기본 라우팅을 참조하십시오. 다음 코드는 매우 기본

expressjs.com

app.get("/go+gle", (req, res) => {
  res.send("google site")
})

더하기(+) 앞의 표현식이 1회 이상 연속으로 반복되면 매칭이 된다. 위 코드는 더하기 앞에 "o"라는 캐릭터가 반복적으로 나타나는 URL 이 매칭된다. 반드시 "o" 앞에 "g" 가 있고 "o" 패턴이 끝난 다음에 "gle" 라는 문자열이 있어야 한다. 아래는 매칭이 되는 URL 일부다. 실험해보자!

/gogle
/google
/gooooooooooogle
app.get("/sylee((mo)+)?", (req, res) => {
  res.send("sylee is definitely shown ! and other string is optional !")
})

물음표(?)는 옵션이라는 의미다. 위 코드는 "sylee" 문자열은 반드시 URL 에 포함되어야 하고 물음표에 의해 그 뒤에 따라오는 문자열은 옵션이다. 없어도 되고 있으면 (mo)+ 에 의해 mo 라는 문자열이 반복(+)되는 패턴이다. 아래 URL 목록은 모두 매칭이 된다. 실험해보자!

/sylee
/syleemo
/syleemomo
/syleemomomomo

정규표현식으로 표현한 URL의 예시

 

Regular Expression for Route Params in Express.js

As per various docs and blogs, as well as this question and others, I'm aware that one can validate route parameters by using regular expressions. This, however, has had me searching for roughly an...

stackoverflow.com

 

URL 이 문자열이 아닌 순수 정규표현식의 예는 아래와 같다. 

app.get(/^\/users\/(\d{4})$/, (req, res) => {
  console.log(req.params)
  res.send(`user id ${req.params[0]} found successfully !`)
})

사용자 컬렉션(users)에서 ID 값을 이용하여 특정 사용자를 조회한다. ID 값은 d{4} 에 의해 숫자 4자리만 허용된다. d는 숫자, {4}는 4자리만 허용한다는 의미다. 즉, /users/1234 /users/5678 등과 같은 URL 만 매칭이 된다. ID 값은 요청객체인 req의 params 프로퍼티로 조회 가능하다. 위 URL의 정규표현식의 의미를 조금 더 설명하면 ^는 시작의 의미다. ^ 뒤에 따라오는 / 로 시작하고 users 라는 문자열이 나오고 다시 /가 따라오고 숫자가 4자리로 끝나면 URL 매칭이 이루어진다. $는 앞에 있는 숫자 4자리로 끝나는지 검사한다. /와 d 앞에는 역슬래쉬(\)가 붙는다. 또한 전체 정규식은 시작과 끝에 모두 /로 감싼다. 

// /users/1234/sun/434 /users/5678 /users/3
app.get(/^\/users\/(\d{4})\/sun\/(\d{3})$/, (req, res) => {
  console.log(req.params)
  res.send(`user id ${req.params[0]} ${req.params[1]} found successfully!`)
})

req.params[0] 인 이유는 /users/{1234}와 같이 변경되는 부분이 한군데 뿐이기 때문이다. 만약 위 코드와 같이 /users/{1234}/sun/{567}과 같이 변경되는 부분이 두군데라면 req.params[1]로 두번째 파라미터를 조회할 수 있다. 

 

하지만 URL 파라미터 값을 숫자 0번으로 조회하는건 의미가 부족하다. 그래서 파라미터 이름으로 조회할 수 있도록 해보자.

app.get("/users/:userId([0-9]{4})", (req, res) => {
  console.log(req.params)
  res.send(`user id ${req.params.userId} found successfully !`)
})

순수 정규표현식으로 작성한 것과 의미는 동일하다. 사용자 컬렉션(users)에서 ID 값을 이용하여 특정 사용자를 조회한다. 차이점은 req.params 객체의 0번으로 ID 값을 조회하는 대신 userId 라는 프로퍼티로 조회가 가능하다는 점이다. 또한 \d 대신 [0-9]라는 대괄호를 사용하여 정규표현식을 표현하였다. 또한, userId 라는 파라미터 이름 뒤에 괄호로 정규표현식을 묶어주었다. 

 

라우트 핸들러 (route handler)

app.get("/hello", (req, res) => {
  res.send("hello world !")
})

get 메서드의 두번째 인자로 넣어준 화살표 함수를 라우트 핸들러 함수 또는 미들웨어 함수라고 한다. 사용자가 입력한 URL에 대해 서버에서 처리할 로직을 의미한다. 예를 들어 위 코드처럼 사용자가 브라우저 주소창에서 /hello 라고 입력하고 엔터키를 치면 두번째 인자로 설정한 라우트 핸들러가 실행된다. 라우트 핸들러는 다양한 형태로 설정이 가능하다.  

라우트 핸들러 예시 참고 (라우터 핸들러 파트)

 

Express 라우팅

라우팅 라우팅은 애플리케이션 엔드 포인트(URI)의 정의, 그리고 URI가 클라이언트 요청에 응답하는 방식을 말합니다. 라우팅에 대한 소개는 기본 라우팅을 참조하십시오. 다음 코드는 매우 기본

expressjs.com

 

만약에 페이스북과 같은 SNS 사이트에서 내가 작성한 댓글을 권한이 없는 다른 사용자는 수정하지 못하게 해야 한다고 해보자!

app.get(
    "/users/:name/comments",
    (req, res, next) => {
      if (req.params.name !== "syleemomo") {
        res.status(401).send("you are not authorized to this page !")
      }else{
        next()
      }
    },
    (req, res) => {
      res.send("this is page to update your comments!") //  댓글 수정 페이지 보여주기
    }
)

위 코드와 같이 작성할 수 있다. 2개 이상의 라우트 핸들러(콜백함수)로 하나의 URL을 처리할 수 있다. 각각의 콜백함수는 콤마(,)로 구분한다. 그리고 마지막 콜백함수를 제외한 모든 콜백함수에는 세번째 인자로 반드시 next 라는 함수 참조값이 필요하다. 

해당 코드는 URL 파라미터로 전달한 사용자 이름이 "syleemomo"와 일치하면(/) 첫번째 콜백함수의 조건문은 만족하지 못하고 next() 함수에 의해 두번째 콜백함수로 사용자 요청이 전달된다. 그러면 두번째 콜백함수에서 사용자에게 댓글 수정 페이지를 보여주면 된다. 즉, 사용자가 브라우저 주소창에 입력한 URL이 /users/syleemomo/comments 라면 "this is page to update your comments!" 메세지가 출력된다. 그 이외의 사용자 이름을 입력하면 HTTP 상태코드 401 (권한없음)와 함께 해당 메세지가 브라우저 화면에 출력된다. 예를 들어, /users/kim/comments 라면 "you are not authorized to this page !" 메세지를 출력한다.

실제 코드에서는 파라미터 값으로 조회한 사용자 이름을 DB 에서 조회한 다음 권한이 없으면 401 에러를 전달할 것이다.  

const blockFirstUser = (req, res, next) => {
  if (req.params.name === "kim") {
    res.status(401).send("you are not authorized to this page !")
  }
  next()
}
const blockSecondUser = (req, res, next) => {
  if (req.params.name === "park") {
    res.status(401).send("you are not authorized to this page !")
  }
  next()
}

const allowThisUser = (req, res) => {
  res.send("you can see this home page !")
}

app.get("/home/users/:name", [
  blockFirstUser,
  blockSecondUser,
  allowThisUser
])

만약 SNS 서비스에서 특정 사용자들이 문제를 일으켜서 블랙리스트에 추가하고 특정 페이지는 볼 수 없도록 해보자! 

위 코드와 같이 작성할 수 있다. 2개 이상의 라우트 핸들러(콜백함수)를 배열에 담아서 하나의 URL을 처리할 수 있다. 마지막 콜백함수를 제외한 나머지 콜백함수는 세번째 인자로 반드시 next 라는 함수 참조값이 필요하다. 해당 코드는 블랙리스트에 등록된 "kim" 과 "park" 은 해당 SNS 서비스의 홈(home) 페이지에 접근할 수 없다. 즉, 401 권한오류와 함께 브라우저 화면에 "you are not authorized to this page !" 메세지가 출력된다. 이에 반해 그 이외의 사용자들은 홈(home) 페이지에 접근 가능하다. 아래는 접근 가능한 사용자 목록이다. 실험해보자!

/home/users/syleemomo
/home/users/sunrise
/home/users/영희
/home/users/철수
/home/users/길동

다만, 이렇게 하고, /home/users/kim 이나 /home/users/park URL 에 접속하면, 아래와 같은 에러가 발생하는데 이는 res.send 로 브라우저 응답을 보내고 나서 아래쪽에 있는 next 함수가 여전히 실행되기 때문이다. 

브라우저 응답을 보내고 나서 라우트핸들러 함수를 종료하려면 아래와 같이 반드시 return 을 설정해줘야 한다. 

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

const blockFirstUser = (req, res, next) => {
    if (req.params.name === "kim") {
      return res.status(401).send("you are not authorized to this page !")
    }
    next()
}
const blockSecondUser = (req, res, next) => {
    if (req.params.name === "park") {
        return res.status(401).send("you are not authorized to this page !")
    }
    next()
}

const allowThisUser = (req, res) => {
    res.send("you can see this home page !")
}

app.get("/home/users/:name", [
    blockFirstUser,
    blockSecondUser,
    allowThisUser
])
app.listen(port, () => {
    console.log(`Example app listening on port ${port}`)
})

 

next 콜백함수

app.get("/chance", (req, res, next) => {
  if (Math.random() < 0.5) return next()
  res.send("first one")
})
app.get("/chance", (req, res) => {
  res.send("second one")
})

next 콜백함수는 사용자 요청을 다음 라우팅 로직이나 다음 미들웨어로 넘겨준다. 위 코드는 /chance 라는 같은 URL 주소에 대해 if 문 안의 조건을 만족하면 요청을 그다음 라우팅 로직으로 넘겨서 "second one" 이 브라우저 화면에 출력된다. 하지만 조건을 만족하지 않으면 브라우저에 "first one" 이 출력되고 두번째 라우팅 로직은 실행되지 않는다. 즉, 사용자가 같은 URL 에 접속하더라도 특정 조건에 따라 서로 다른 로직을 처리하는 경우에 유용하다. 

app.get(
  "/fruits/:name",
  (req, res, next) => {
    if (req.params.name !== "apple") return next()
    res.send("[logic 1] you choose apple for your favorite fruit !")
  },
  (req, res, next) => {
    if (req.params.name !== "banana") return next()
    res.send("[logic 2] you choose banana for your favorite fruit !")
  },
  (req, res) => {
    res.send(`[logic 3] you choose ${req.params.name} for your favorite fruite !`)
  }
)

next 콜백함수의 조금 더 실용적인 활용을 살펴보자. /fruits 라는 URL 하위에 사용자가 좋아하는 과일을 파라미터로 추가한 경우에 과일 종류에 따라 서로 다른 로직을 처리해야 한다고 해보자. 예를 들면, /fruits/apple /fruits/banana /fruits/watermelon 등의 URL을 사용자가 주소창에 입력하고, 입력한 과일의 이름을 서버에서 응답으로 전송해서 브라우저 화면에 출력한다고 가정해보자. 

위 코드는 만약 사용자가 /fruits/apple 이라고 입력하면 첫번째 콜백함수의 조건문을 만족하지 않으므로 브라우저로 "[logic 1] you choose apple for your favorite fruit !" 라는 응답을 전송해주고 로직이 끝난다. 하지만, /fruits/banana 를 입력하면 첫번째 콜백함수의 조건문을 만족하므로 next() 함수가 실행되서 다음 로직으로 사용자 요청을 넘겨준다. 그럼 두번째 콜백함수에서 조건문은 만족하지 않으므로 "[logic 2] you choose banana for your favorite fruit !" 을 응답으로 전송하고 로직은 끝난다. 만약, apple 이나 banana 가 아닌 제 3의 과일 이름을 입력하면, 첫번째와 두번째 콜백함수의 조건문을 모두 만족하므로 next() 함수에 의해 사용자 요청이 마지막 콜백함수로 넘어가서 마지막 로직이 실행된다. 예를 들어, 아래와 같은 URL 로 사용자 요청을 보내서 실험을 한번 해보자!

/fruits/kiwi 
/fruits/watermelon 
/fruits/orange
/fruits/사과
/fruits/바나나
/fruits/수박

위 URL 목록 전부 apple 이나 banana 가 아닌 제 3의 과일에 해당하므로 next() 함수에 의해 사용자 요청이 맨 마지막으로 넘어가서 마지막 로직만 실행이 된다. 사과, 바나나, 수박은 당연히 한글이므로 apple 이나 banana 와 같은 영문자와 다르다. 결론적으로 사용자 요청 URL 의 파라미터에 따라 서버에서 서로 다른 로직을 처리해야 하는 경우에 next() 함수를 활용할 수 있다. 

 

요청 객체 (request object)

요청 객체는 express 서버에서 사용자 요청에 대한 정보를 확인(참조)할 수 있는 기능을 제공한다. 자주 쓰이는 요청 객체의 파라미터는 3가지 정도로 요약된다. 

app.get("/users/:id", (req, res) => {
  res.send(`user id - ${req.params.id}`)
})

req 객체의 params 프로퍼티는 사용자가 요청한 URL 의 파라미터(parameter) 정보를 조회한다. 예를 들면, /users/1234 와 같은 URL 주소를 요청하면 패턴매칭에 의하여 id 값을 req.params.id 로 조회할 수 있다. 

app.get("/shirts", (req, res) => {
  res.send(`feature - color : ${req.query.color} / size: ${req.query.size}`)
})

req 객체의 query 프로퍼티는 사용자가 요청한 URL 의 쿼리스트링(query string) 정보를 조회한다. 예를 들면, /shirts?color=red&size=large 와 같은 URL 주소를 요청하면 req.query.color 와 req.query.size 로 쿼리스트링 값을 얻을수 있다. 

app.post("/user", (req, res) => {
  res.send(`${req.body.user}`)
})

req 객체의 body 프로퍼티는 사용자가 요청한 URL 의 요청본문(request body) 정보를 조회한다. 

 

응답 객체 (response object)

응답 메서드

app.get("/hello", (req, res) => {
  res.send("hello world !")
})

서버에서 문자열, 객체, 배열 등을 클라이언트로 보낸다. 문자열은 HTML 문서로 간주한다. 

app.get("/hello", (req, res) => {
  res.send(`<html>
                <head></head>
                <body>
                    <h1>Hello world !</h1>
                    <input type='button' value='Submit'/>
                </body>
            </html>`)
})

간단한 HTML 문서를 전송해서 웹화면을 구현할 수도 있다. 

app.get("/user", (req, res) => {
  res.send({ user: "syleemomo" })
})

객체나 배열은 json 응답으로 보낸다. 즉, res.json 메서드와 동일하다. 

app.get("/hello", (req, res) => {
  res.json({ user: "syleemomo", msg: "hello !" })
})

서버에서 json 응답을 클라이언트로 보낸다.

app.get("/google", (req, res) => {
  res.redirect("https://google.com")
})

서버에서 리다이렉션 주소를 클라이언트에게 응답으로 보낸다. 클라이언트는 해당 주소로 재요청을 보낸다. 

app.get("/hello", (req, res) => {
  res.render("index")
})

서버에서 HTML 문서를 클라이언트에게 응답으로 보낸다. index는 HTML 문서이다. 

app.get("/user", (req, res) => {
  res.status(401).send("you are not authorized !")
})

HTTP 상태 코드(status code)를 클라이언트에게 응답으로 보낸다. 

 

라우터 모듈

var express = require('express');
var router = express.Router();

// middleware that is specific to this router
router.use(function timeLog(req, res, next) {
  console.log('Time: ', Date.now());
  next();
});
// define the home page route
router.get('/', function(req, res) {
  res.send('Birds home page');
});
// define the about route
router.get('/about', function(req, res) {
  res.send('About birds');
});

module.exports = router;
var birds = require('./birds');
app.use('/birds', birds);

라우터 모듈은 해당 URL 의 하위 URL 과 그들의 처리로직을 모듈 형태로 만들수 있다. 예를 들면, /birds 의 하위 URL 인 / 과 /about 의 처리로직을 모듈 형태로 분리할 수 있다.

 

브라우저 캐싱(cashing)

참고자료

 

Express.js 서버는 왜 304를 반환하는 걸까?

김코딩 님이 잘하고 싶어서 만든 블로그

huns.me

 

728x90