240906 TIL Node 숙련 2주차 1, 에러가 불꽃처럼 터져

LIHA·2024년 9월 6일
0

내일배움캠프

목록 보기
39/54
post-thumbnail

알고리즘

순열조합을 구현하려면 재귀함수가 필요하다!

참고한 블로그

배열과 숫자를 인자로 받아 그 배열 중 숫자만큼 조합으로 뽑아달라는 재귀함수가 구현된다. 이를 참고해 구현한 조합함수는 아래와 같다.

내가 필요한 것은 3개를 랜덤으로 추출하는 함수였다.
다만 기주 튜터님의 말씀대로 순열조합은 원하는 케이스만 체리픽 할 수 없고, 모든 케이스가 다 나온다. 그래서 너무 길면 안 돌아간다. 하여 문제나 원하는 답의 길이를 보고 대강 사용할 알고리즘을 유추할 수 있는 능력도 필요하다고 말씀 주셨다.


Node

쿠키🍪 는 뭐고 세션은 뭔가요

쿠키 는 편하지만 보안에 취약하다!
그래서 데이터를 서버에서 관리하게 만든 것이 세션.

  • res.end()는 아무 response도 보내지 않고 그냥 끝내는 것이다.
    보통 res.status().json() 타입으로 쓰는 경우가 많았는데 때로는 응답값을 보낼 필요가 없기도 하다. (쿠키를 전송할 때 라던지) 그럴 땐 res.end()로 그냥 종료해주자. (마치 void 같은 느낌)

모든 쿠키를 조회하는 방법은 여러가지, 선택은 나의 몫

const cookie든 cookies든 클라이언트의 모든 쿠키를 조회할 수는 있다. 다만 형태가 다를 뿐이다.

req.headers.cookie; 를 쓰면 위와 같이 나오고, req.cookies;를 쓰면 아래와 같이 나온다.

JWT는 Header.payload.signature 이다. like 개미🐜 (머리 가슴 배)

그래서 qwer~.asdf~.zxcv~ 형태로 되어 있다.
signature 부분은 해시함수로 암호화 된다. 보통 SHA256을 쓴다.

  • 헤더는 토큰의 타입과 암호화 형태에 대한 정의가 들어있다. (HS256 처럼 256바이트로 해싱되었다 등...)
  • 페이로드는 실제 전달하려는 데이터가 들어있다. 그래서 여기 암호같은 민감정보 넣어서 전달하면 안된다.
  • 시그니처는 헤더, 페이로드, 비밀키를 이용해 생성된다. 이 서명부는 토큰의 변조 여부를 확인할 수 있게 해준다.
  • payload 값들 맨 뒤에 iat라는 값이 붙는데 생성날짜다. JWT가 자동으로 만드는 것. 그래서 expire를 정해주면 몇 초가 차이나는 지 볼 수 있다.

JWT의 특성

  • 비밀 키를 모르더라도 복호화는 누구나 가능하다.
  • 그래서 변조만 불가능 할 누구나 데이터에 접근을 할 수 있다.
  • 특정 언어에서만 사용할 수 있는 게 아니다. (실제로 JAVA에서도 썼었지)
    -> JWT는 일종의 데이터 형식이고, 개념적인 것. 실제 구현 및 사용은 우리가 알아서 해야한다.

정말 중요한 두 가지 특징
▶인증 서버에서 발급되었는지 위변조 여부를 확인할 수 있다. jwt.verify(token, 'secretkey') 형식으로 시크릿키 값을 써주면 ok.
▶복호화는 누구나 가능하기 때문에 중요정보를 payload에 넣으면 안된다.

그래서 JWT와 쿠키/세션이 뭐가 다른거지? 🤔

쿠키/세션은 데이터 교환 및 관리 방식에 대한 개념이고, JWT는 그때 사용되는 데이터 표현방식의 일종. 즉, JWT가 쿠키로 세션에 저장된다- 라고 표현할 수 있는 것.

  • JWT 데이터는 접근만 가능하지 변조가 어렵고, 서버에 상태를 저장하지 않아서 서버를 Stateless로 관리할 수 있다.
  • 그런데 쿠키나 세션은 로그인 정보나 세션 데이터 등을 서버에 저장해서 상태를 유지한다. 하여 Stateful 상태로 데이터가 관리된다.
  • Stateless - 서버가 죽었다 살아나도 항상 같은 동작을 하는 것
  • Stateful - 서버가 죽었다 살아나서 다른 동작을 하는 것
    -> 서버 스스로가 어떤 기억을 갖고 어떤 동작을 하는 지의 차이!
    -> 그러므로 로그인 정보가 서버에 저장되는 것은 무조건 Stateful이다.

PRIMARY KEY 설정 자체가 NOT NULL과 UNIQUE를 모두 의미한다

ALTER가 뭔 기능인지 기억이 나지 않아 내 블로그를 뒤지다가, 쓴 기억도 나지 않는 항해때 TIL 에서 ALTER TABLE MODIFY를 이용해서 id에 AUTO_INCREMENT를 붙이는 것을 발견했다.
또다른 항해 TIL 에 의하면 ALTER는 DDL이라 DB에 직통으로 영향을 미치고, 얼터 자체가 '바꾸다' 라는 뜻을 가지고 있어서 이미 존재하는 개체의 특성 변경에 쓴다고.

아항! 그렇구만.

Prisma에서 NOT NULL 어떻게 쓰더라? -> 타입에 물음표 안 붙이면 된다

  • Prisma는 저렇게 타입에 물음표 붙는 순간 NULLABLE : true가 된다. null일 수도 있다는 것이다. 대충 예전에 정섭 튜터님이 말씀해 주셨던 유니온 같은 느낌이다.

얘는 왜 userId인데 unique 안 붙어요? -> 1:N 관계라서!

한 명의 유저가 여러개의 게시글을 쓸 수 있으므로 1:N 관계이기 때문에, 여러 게시글에 잡히는 userId가 중복될 수 있다. 그러므로 unique를 걸면 안된다.

지금은 단순히 테이블 선언이 아니라 관계 맵핑이기 때문에, 어떤 Id가 어떤 테이블에서 1과 N중에 어떤 측으로 작용할 지 고려해보자.

-> 그래서 Users에 나타나는 Posts는 이렇게 표시한다. 여러개를 나타내기 위해 배열연산자를 사용해줘야 한다.

RDB 테이블 관계 맺는게 헷갈린다면 -> 누가 부모인지를 정하고 들어가자!

이건 관계에 따라 물론 어느쪽도 부모가 될 수도 있지만 (N:N인 경우) 보통은 1:N이 많고 1:1도 있기 때문에 1인 쪽을 먼저 잡아주고 들어가면 된다.

.env에 DB URL 쓸때 맨 뒤에 DB이름을 꼭 수정해주자

저 부분을 수정하지 않으면 예전 DB를 계속 바라보면서 작동하므로 예전 DB에 INSERT하는 대참사가 일어날 수 있다.

DB주소를 무사히 수정했다면 터미널에 npx prisma db push를 입력해서 작성한 쿼리를 그대로 쏴주도록 하자.

Prisma DB 생성 때 만들지 않은 테이블 컬럼의 데이터를 넣으려 하면 터진다

userInfos 테이블에 password라는 컬럼은 넣어주지 않았다.

그런데 인자에서 받아버리려 해서 에러가 터졌다.

  • 내가 자꾸 혼동하는 것 : Insomnia에서 데이터를 쏴주는 것과 해당 테이블에 저장이 되는 것은 별개이다. 애초에 처음에 req.body;에서 뭘 받기로 했는지를 보자.

userInfos 테이블에 password가 없어도 user가 password를 받기 때문에 필요하다.

유령계정이 되지 않으려면 -> 트랜잭션 개념이 필요하다

사용자 등록은 되었지만 사용자 정보등록에 실패해서 로그인만 가능한 경우가 생긴다고 가정해보자. 이 사용자는 어떻게 해야 할까?
이런 불상사를 대비해서 우리는 트랜잭션(Transaction) 이라는 개념을 알아야한다. 트랜잭션은 될거면 아예 끝까지 되고, 안될거면 애초에 아무것도 수행되지 않는 것이다.

JWT의 payload에 쌩짜로 암호 담아 보내면 안되는데? -> 이걸 위해 필요한 bcrypt

bcrypt 설치에 실패했다면 -> node-pre-gyp 패키지 먼저 전역설치 해주자

물론 나는 설치에 성공했지만 내 노트북은 실패할 수도 있으니까 방법을 남겨둬야지.
yarn add -g node-pre-gyp 라는 명령어로 node-pre-gyp 패키지를 먼저 전역으로 설치해준 다음에 yarn add bcrypt 를 다시 쳐주면 잘 될 것이다. 그래도 안되면 포기하자

Users 테이블은 userInfos 테이블을 같이 끌어와야 하는데 어쩌지 -> 중첩 select문이 있다

Users 테이블과 userInfos 테이블은 1:1 관계를 맺고 있어 Users와 UserInfos는 같이 끌려나와야 한다. 그런데 지금까지의 문법대로면 따로따로 조회할 수 밖에 없다.
내가 원하는 것은 [Users[userInfos]] 이런 구조일텐데, 실제 실행된 것은 [Users] - [userInfos] 가 될 것 같다는 얘기.

▶ 이걸 위해 존재하는게 중첩 select문.

놀랍게도 select문 안에 이런 식으로 쓸 수 있는데, 이걸 이렇게 쓸 수 있는 이유는 Prisma에 아래와 같이 되어있기 때문.

여기서 userInfos와 1:1 관계로 선언해주었기 때문에 같이 머리채잡혀 끌려나올 수 있어서 저렇게 써도 문제가 없는 것이다.

미들웨어는 통통 튀어다닌다 - 어디에도 넣을 수 있다!

튜터님이 그려주신 작동 순서. URL이 /users가 맞으면 import 시켜둔 authMiddleware 로직 타게 해주고, 그 다음에 우리가 쓴 비동기 함수 로직 타게 해줘. 가 된다.

중첩 select는 SQL의 JOIN과 같다!

중첩 select는 SQL의 JOIN과 동일한 역할을 수행한다.
다만 이 문법을 사용하기 위해서는 Prisma model에서 @relation() 구문으로 관계 설정을 미리 해주었어야 한다. 이걸로 Prisma는 현재 모델에서 참조하는 외래 키를 인식하고 SQL을 생성할 수 있게 되기 때문.

-> 현재 테이블과 연관된 모든 컬럼을 조회하고 싶다면 include 문법으로도 조회할 수 있다고 하는데, 이건 뭔 말인지 잘 모르겠다.

prisma 폴더랑 .env가 없어요?! -> npx prisma init 을 입력하자

그렇다면 .gitignore까지 짜잔 하고 나타나 줄것이다. 단 DB주소 설정 잊지 말것. 수정하지 않으면 엉뚱한 DB를 바라볼 수 있다.

순서는 뭘까? 정섭 튜터님의 발견

a b d e c?
-> 정답은 b d c a e. 튜터님은 bcade 라고 생각하셨다고

console.log니까 바로 b 딸려나가고, asyncF 호출되니까 d 잡혀나가고, 아래 await 걸리니까 버리고 sync() 먼저 들어가고... 오 의외로 이게 맞구나. 근데 왜지?

▶ async 함수라고 해도 await 만나기 전까진 동기적이다.
그리고 async 함수가 '반환하는 애'가 Promise인거고, Promise중에 가장 먼저 Resolve 되는 애들끼리 실행되는 건데 비동기를 동기적으로 쓰고 싶어서 async / await를 쓰고 난리를 치는 것

▶ 노드는 싱글스레드다. 그렇다고 해서 실제 비동기로 병렬프로그래밍 되는건 아니다. OS한테 맡길건 맡기고, Promise 객체 같은건 이벤트 루프에 돌리는것. 이벤트 루프에 넣기 전에 쌓아놓는 곳을 마이크로 태스크 큐 라고 하는데, 그게 진짜 큐 구조는 아니다. 이게 왜 진짜 큐가 아니냐면 A 연산 1초, B 연산 10초라고 하면 A가 1초임에도 불구하고 B가 먼저 왔다고 B가 먼저 돌아가는 일은 일어나지 않는다.

구조분해할당이 정확히 뭐더라?

구조분해 할당... 저렇게 분해하는 거인건 알겠는데... 뭐지? MDN 도와줘! MDN이 알려주는 구조분해할당

구조 분해 할당 구문은 배열이나 객체의 속성을 해체하여 그 값을 개별 변수에 담을 수 있게 하는 JavaScript 표현식입니다.

  1. 함수에 객체나 배열에 저장된 데이터의 일부만 전달하고 싶을 때
  2. 함수의 매개변수가 많을 때
  3. 매개변수의 기본값이 필요할 때

기타등등 이럴때 쓴다고 한다.

app.~ 하고 router.~ 하고 뭐가 다른걸까? 라우터와 미들웨어 개념

참고 블로그1
참고 블로그2

라우터는 (req, res)가 붙는 것인데 리퀘스트 경로를 보고 해당 요청 정보를 처리할 수 있는 곳으로 기능을 전달해준다고 한다. 참고 블로그에는 클라이언트의 요청 경로에 따라서 그것을 담당하는 함수로 분리시킨다. 라우터로 등록하려면 get() 메소드를 호출하여 등록할 수 있다. 라고 써있다.
미들웨어를 등록할때는 use() 로 등록한다고 한다. 그러니 app.use() 가 전역 미들웨어 등록인 셈.

에러처리 미들웨어는 전역 미들웨어 중 최하단에 위치해야 한다!

app.use()~ 들 중에서 제일 아래에 포트 열기 직전에 써줘야 한다. 그래야 앞선 전역 미들웨어들 로직 다 타고 나서 맨 마지막에 에러처리 미들웨어를 탈 수 있기 때문!
-> 단, 에러 메시지는 너무 구체적으로 쓸 필요는 없다. 어떤 문제인지 알게 되면 해킹이나 디도스 등의 공격을 받을 수도 있기 때문.


트러블슈팅

내 코드는 왜 자꾸 비밀번호가 틀렸다며 로그인이 안 될까? -> 비교조건절이 틀려서!

나는 여기서 새로 hash를 걸어 salt를 10번 정도 돌렸는데 이러면 새로 암호화된 다른 암호가 나올 것이다. 이때는 bcrypt.compare(비교할 pwd, DB의 pwd) 를 써줘서 암호화 했을때 둘이 같은지를 보는게 낫다.

같은 구절인데 왜 제 코드만 터지나요 -> 위치가 잘못되어서!

  • 어여쁘게 잘 돌아가는 튜터님의 코드

  • 에러가 쿠와아악 터지는 나의 코드

난 그냥 const를 쓰고 싶었을 뿐인데,

이렇게 에러가 펑펑펑 터진다. 뭐가 문제인걸까?

저 await bcrypt절을 if(!pwdCompare)에 그대로 집어넣으면 터지지 않아서, 똑같은 코드인데 왜 내껀 터지는지가 궁금했다.

▶ 이렇게 구문배치를 하고 내가 Insomnia로 쐈던 것은 password가 빈게 아니라 없는 email, 즉 유저 자체가 존재하지 않는 경우였다! (원래 목적은 사용자가 존재하지 않습니다. 라는 메시지를 보고싶었다.)

▶ 이때 const pwdCompare의 위치때문에 하단의 if(!user) 절에 걸리지 못하고 const절에 먼저 걸려버려서 DB에 존재하지 않는 password를 비교하려니 당연히 여기서 터진 것.

▶ 기주 튜터님도 이 코드를 보시고는 password에서 터진 게 아닐거라고 하셨다. 그 말씀대로 에러 메시지도 user.password를 가리키고 있었다.

▶ 여기서 const로 pwdCompare를 빼주고 싶으면 if(!pwdCompare) 위에 엔터치고 선언했으면 문제 없었을 거라고 하셨다.

얘 왜 userId를 못 불러오지? -> await를 빼먹었네!

이 코드가 아래처럼 자꾸 where절에서 터지는 바람에 튜터님께 문의

userId가 missing이라고? DB에는 잘 들어가 있는데 어딘가 못 불러오는 것 같았다. 왜일까? 창민 튜터님께 여쭈러 가서 console.log를 찍어보았다.

놀랍게도 비동기 함수 속에 있는 user인데다 Promise 객체를 반환하는 prisma를 호출하면서도 앞에 await를 써주지 않고선 실행이 안된다고 울부짖는 나였다. 이렇게 참조중인 다른 부분에서 에러가 터지니 알 도리가 없는 것은 덤. 아직 갈 길이 멀다.

ES6 문법에서 시크릿키 같은 환경변수를 .env에 숨기고 거기서 불러오고 싶은데 -> 해결!

이야호!! 드디어 토큰이 발급된다아아!!!

코드는 아래와 같다. 방법은 이미지 아래 기재.

  1. yarn add dotenv로 설치하고 ES6 기준이므로 import dotenv from 'dotenv' 로 import 해준다. (주석처리하니 안 됐다)
    -> 이건 브라우저의 DOM 객체처럼 Node.js에서만 쓸 수 있는 라이브러리(?) 이므로 다른 곳에서는 쓸 수 없다고.
  2. dotenv.config() 를 입력해준다.
  3. process.env.PORT 같이 .env에 저장해준 환경변수를 그대로 불러와서 쓰거나, 구조분해할당으로 쓴다.
    const {PORT} = process.env 이렇게 쓰는게 구조분해할당.

그러나 환경변수 구조분해할당이 안좋을 수도 있다고? -> Next.js에서는 돌아가지 않는다고 한다.
참고 블로그


잡담

과제 물리치고 5조

이번 조는 다시 복작복작한 느낌이라 좋다

아니 저기요? 여러분? 왜 이렇게 된거지

PM 11:57. 집에 가염!

profile
갑자기 왜 춤춰?

0개의 댓글