1. 클라이언트 - 서버 구조

: HTTP는 클라이언트가 request를 보내면 서버가 response를 보내는 구조이다. 즉 클라이언트와 서버가 개념적으로 분리된 형태이며, 클라이언트로부터 request가 안오면 서버는 자신의 힘으로 클라이언트 측으로 HTTP메시지를 보낼 수 없다는 얘기다.(HTTP는 서버가 클라이언트에게 먼저 메시지를 보낼 수 없고 클라이언트의 요청메시지에 대한 응답만 할 수 있다는 이야기임)

 

2. 무상태 프로토콜(stateless)이다

: HTTP는 서버가 클라이언트의 상태를 보존하지 않는 stateless한 프로토콜이다.

 

stateful, 즉 상태를 유지한다고 하면 서버는 클라이언트의 이전 상태를 기억한다. 쉽게 말해 내가 친구랑 맥북에 대해 대화하다가 "그거 살까?"라고 했다면 친구는 '그거'가 맥북을 지칭한다는 것을 알고 있다.stateless, 상태를 유지하지 않는다면 서버는 클라이언트의 이전 상태를 기억 못 한다. 내가 친구랑 맥북에 대해 대화하다가도 "그거 살까?"라고 했을 때 친구는 '그거'가 맥북을 말하는 건지 아이폰을 말하는건지 아이패드를 말하는 건지 모른다는 뜻이다. 따라서 이 상태라면 친구에게 "그거 살까?"가 아니라 "맥북 살까?"라는 식으로 대화해야 한다.

 

즉, stateless는 서버가 이전 상태를 기억하지 못 하기 때문에 매 요청 시마다 필요한 데이터를 몽땅 다 넘겨야 한다! 때문에 얼핏 보면 stateless방식은 전송하는 데이터 양이 더 많으니까 안 좋은 거 아닌가?할 수 있지만 이 방식은 서버 확장성이 높다는 엄청난 장점을 가진다. 생각해보면, stateful방식은 내가 재준이한테 맥북 얘기하다가 다훈이한테 가서 "그거 살까?"라고 하면 다훈이는 그게 뭔데 임마라고 하겠지만, stateless방식은 재준이한테 맥북 얘기하다가 다훈이한테 가서 "맥북 살까?"라고 얘기해도 괜찮다. 즉 stateless방식은 응답 서버를 쉽게 바꿀 수 있는 것이다. 때문에 클라이언트 요청이 폭증해도 서버를 대거 투입할 수 있으며 이론상 무한한 서버 증설이 가능하다. stateful방식은 클라이언트가 서버 A와 통신하기 시작했다면 계속 그 놈과 통신해야 하지만 stateless방식은 아무 서버나 입맛대로 호출해도 된다는 것.

 

3. 비연결성

: 연결을 유지한다는 것은 이는 클라이언트-서버 간에 딱히 주고받을 것이 없는 순간에도 계속해서 TCP/IP연결을 맺고 있는 걸 말하고 이는 서버의 자원을 소모하는 것을 의미한다. HTTP는 기본적으로 연결을 유지하지 않는다 즉 볼일이 끝나면 TCP/IP연결을 종료한다. 이로 인해 최소한의 서버 자원을 유지할 수 있음이 보장된다. 같은 시간동안 수천 수만 명이 서비스를 사용하다 특정 시간대에 서버에서 처리되고 있는 요청은 매우 적을 것이다(검색 기능이 있다고 할 때 동시에 검색 기능을 누르는 일은 많지 않은 것처럼). 따라서 이 비연결성 특성을 통해 서버 자원을 매우 효율적으로 이용할 수 있게 된다.

 

 그러나 이런 비연결성의 단점은 자원 요청 시마다 매번 TCP/IP 연결을 맺어야 하는데 이 작업은 3 way handshake가 수반되므로 overhead가 누적될 수밖에 없다는 것. 이 문제점은 HTTP 지속 연결(Persistent Connections)로 해결됐다.

 

4. 단순하고, 확장 가능하다

 

모두들 알다시피 리액트에서 setter함수는 비동기적으로 동작한다.

즉 호출되는 순간 state를 업데이트하고 다음 코드를 실행하는 방식이 아니기 때문에 예기치 못한 오류들에 직면할 수도 있다.

const [count, setCount] = useState(0);

const onAdd = () => {
  setCount(count + 1);
  console.log(count);
}

// 카운트가 0일 때 onAdd가 실행되면 콘솔에 찍히는 카운트값은 1이 아니라 0일수도 있다

위 상황에선 내가 분명 setCount를 통해 count값을 1 늘려줬는데, 콘솔로그에 찍는 코드에서 참조되는 count값은 1 늘어난 값이 아니라 옛날에 쓰던 count값이 되는 문제가 생긴다. 즉 가장 최신의 count값을 참조해야 하지만 못 하게 되는 것이다. setter함수가 비동기적으로 동작하기 때문에..(이 문제는 클래스형 컴포넌트에서는 두 번째 인자로 갱신 이후 실행할 콜백을 넘겨줌으로써 해결할 수 있고 함수형 컴포넌트에선 useEffect를 통해 해결가능했다)

 

또 문제인 것은 setCount가 여러 번, 예를 들어 100번 호출됐다면 100이 늘어나야 하는데 100보다 덜 늘어나는 상황도 생길 수 있다. 각각의 setCount함수들이 참조하는 count값이 가장 최신의 count값이라는 보장이 없기 때문.

 

const [count, setCount] = useState(0);

const onAdd = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}

// 카운트가 5 늘어날 것이라 예측 가능 but 5만큼 안 늘어났을 수도 있다..

 

이 때 콜백을 써줌으로써 이 문제를 해결 가능하다. 좀 더 풀어서 말하면, 바로 이전의(즉 최신의) state를 사용해 새로운 state를 계산하는 콜백을 setter함수에 전달함으로써 이 문제를 해결할 수 있다. 공식 문서를 참고해보니, 콜백을 사용하는 갱신방법(함수적 갱신)의 경우 바로 이전 state를 사용해서 새로운 state를 계산한다고 한다.

const [count, setCount] = useState(0);

const onAdd = () => {
  setCount(count => count + 1);
  setCount(count => count + 1);
  setCount(count => count + 1);
  setCount(count => count + 1);
  setCount(count => count + 1);
}

 

setCount(count + 1)는 count값을 인자로 전달하므로, 이 때 전달된 값은 나중에 가면 가장 최신의 값이 아닐 수도 있다. 

반면 setCount(count => count + 1)의 경우는 count값이 아니라 실행할 콜백을 인자로 전달하고, 전달된 콜백이 실행되면 그 때서야 그 때의 count값을 참조하므로 이 경우는 가장 최신의 count값을 참조하는 것이다.

Firebase

: Google이 소유하고 있는 모바일, 웹 애플리케이션 개발 플랫폼. 즉 앱을 개발하고 개선할 수 있는 도구들의 모음이다. 인증, 데이터베이스, 푸시메시지 등등을 지원하는 플랫폼으로 이를 활용하면 좀 더 편한 개발이 가능(그런 것들을 하나하나 만들지 않아도 되니까). 백엔드 기능을 클라우드 서비스 형태로 제공하기 때문에 서버리스 어플리케이션 개발을 가능하게 하는 녀석. 

 

공식문서를 참조하면 연동하는 법을 알 수 있으나 내 입장에선 쉽지 않았다..그래서 firebase로 구글로그인을 연동하는 법에 대해 정리한다.

 

https://firebase.google.com/docs/auth/web/google-signin?hl=ko&authuser=0 

 

자바스크립트로 Google을 사용하여 인증  |  Firebase Documentation

Check out what’s new from Firebase at Google I/O 2022. Learn more 의견 보내기 자바스크립트로 Google을 사용하여 인증 사용자가 Google 계정을 사용하여 Firebase에 인증하도록 설정할 수 있습니다. Firebase SDK를 사

firebase.google.com


 

1. 리액트 프로젝트에 Firebase 설치

yarn add firebase

 

2. firebase 콘솔에서 프로젝트를 만들고 SDK를 받는다. 

 

3. firebase 콘솔에서 인증(authentication) 섹션을 열고 로그인 방법 탭에서 구글 로그인을 사용 설정시킴.

 

4. 리액트 프로젝트에 firebase.js(이름은 뭐 자유ㅎㅎ)를 만들고 다음과 같이 코드 작성

import { initializeApp } from "firebase/app";

const firebaseConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_PROJECT_ID,
  storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_APP_ID,
  measurementId: process.env.REACT_APP_MEASUREMENT_ID,
};

// Initialize Firebase
export const firebaseApp = initializeApp(firebaseConfig);

참고로 리액트에서 API key같은 것들을 작성할 땐 보안문제가 있으니 발급받은 거 그대로 쓰지 않는 게 좋다. 프로젝트 폴더에 .env라는 폴더를 만들고, 거기에 REACT_APP_API_KEY = 블라블라 이런 식으로 쓴 다음, 프로젝트에서 사용할 때는 process.env.REACT_APP_API_KEY이렇게 쓰면 됨. 참고로 저렇게 환경변수 이름을 쓸 땐 REACT_APP이란 걸 붙여야 하고, .gitignore에 .env를 작성하도록 한다.

 

5. auth_service.js라는 파일만들고(역시나 이름은 뭐 자유) 다음과 같이 만들어준다.

import { 
	getAuth, 
	signInWithPopup, 
	GoogleAuthProvider, 
	signOut } from "firebase/auth";
import { firebaseApp } from './firebase'; // 작성한 firebase.js를 불러오는 용도

class AuthService{
	constructor(){
		this.firebaseAuth = getAuth();
		this.googleProvider = new GoogleAuthProvider();
	}

	login(){
		return signInWithPopup(this.firebaseAuth, this.googleProvider);
	}

	logout(){
		signOut(this.firebaseAuth);
	}
}

export default AuthService;

참고로 클래스 형태가 아니라 함수로 만든 다음 그걸 export하는 형태로 만들어도 상관없다.  또한 아까 작성한 firebase.js의 코드를 실행시켜 firebase app을 초기화해야 하기 때문에 6번 줄에서 import하는 모습.

 

 

6. index.js에서 작성했던 auth_service.js를 활용해 dependency injection

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.module.css';
import App from './app';
import AuthService from './service/auth_service';

const authService = new AuthService();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App authService={authService}/>
  </React.StrictMode>
);

이를 통해 app.jsx에서는 authservice.login을 통해 만들어둔 로그인 기능을 활용할 수 있다.

본 포스트는 아래 영상을 요약한 글로 원작자의 허락을 받고 올립니다. 저작권 문제시 본 글은 삭제조치하겠습니다

https://www.youtube.com/watch?v=1QiOXWEbqYQ&t=1s 

 


로그인? 아이디랑 비번 DB에 갖고있다가 로그인할 때는 그게 DB에 있는지 조회하면 될 줄 알았지만 그건 너무 날먹이었고~ㅋㅋㅋ 보안문제땜에 당연히 그렇게 안 한다고 한다.

 

로그인 기능을 만들기도 쉽지 않지만, 더 까다로운 건 로그인 상태를 유지하는 것! 지메일에 로그인했는데 메일 열 때마다 뭐 할때마다 로그인할 수는 없다. 내가 로그인하면 로그인했다는 걸 서버가 계속 알고 있어야 한다. 인증(Authentication)과 인가(Authorization)의 차이가 중요한 것.

 

JWT는 인가 즉 Authorization에 관련된 기술로, 어떤 사이트나 서비스에 사용자가 로그인했다가 서버에 인지시키는 것과 관련된다.

 

인가란 분야에서 전통적으로 많이 사용된 건 세션(session)이다. 사용자가 로그인에 성공하면 서버는 세션표딱지를 출력하는데 이걸 쭉 찢어서 반쪽은 브라우저라 보내고 반쪽은 자기가 갖는다. 브라우저는 이를 session ID라는 이름의 쿠키로 저장한다! 브라우저는 그럼 로그인된 사이트에 요청을 보낼따마다 이 표딱지를 실어보내고, 서버는 요청에 표딱지반쪽이(즉 session ID가) 서버는 자신이 가진 표딱지들 중 브라우저로부터 받은 반쪽짜리와 맞는 표딱지가 있는지 보고 ‘인가’한다. 이렇게 session ID를 사용해 서버에 로그인되어있음이 지속되는 상태를 ‘세션’이라고 표현한다. 그러나 이는 서버쪽 메모리가 날아가거나(재부팅 등으로) 너무 많은 유저들이 로그인하면 여러 반쪽표딱지들을 갖고 있어서 메모리가 부족해지는 현상이 생기기도 한다. 또한 여러 서버를 운영할 경우 로드밸런싱 문제로 로그인은 A 서버에서 하고(반쪽표딱지가 A 서버 메모리에 저장됨) 인가는 B 서버에서 진행될 수도 있는데, B 서버에는 A 서버에서 로그인한 반쪽표딱지가 없으니 에러가 생기기도 한다..

 

그래서 이런 부담없이 고안된 인가 방식이 토큰 방식인 JWT다. JSON WEB TOKEN의 줄임말이다. 토큰방식은 로그인하면 토큰이랑 딱지를 주는데, 세션방식과는 달리 반으로 찢지 않고 그대로 준다. 이는 서버가 뭔가를 기억하지 않는다는 얘기다! 토큰은 알파벳과 숫자들이 마구잡이로 섞인 문자열로 인코딩 또는 암호화된 3가지 데이터를 이어붙인 거다. XXXXXX.YYYYYY.ZZZZZZ같은 형태이며 마침표를 기준으로 세 가지 부분(헤더, 페이로드, 서명)으로 구분한다.

Payload

디코딩하면 JSON형식으로 여러 정보들이 들어있다. 토큰을 누가 누구에게 발급했는지, 언제까지 유효한지 등..토큰에 담긴 사용자 정보 등의 데이터를 Claim이라고 한다. 이를 통해 로그인 이후 요청들마다 사용자로부터 서버로 이 Claim들이 전해지는데, 사용자가 발급받아 갖고 있는 토큰 자체에 이런 정보들이 들어있으면 서버는 요청마다 하나하나 DB에서 뒤질 필요가 없으니 더 편하다. 근데 디코딩이 매우 쉬워서 누군가 수정할 오류가 있다! 근데 조금이라도 바뀌면 금방 알아챌 수 있다. 어떻게 하는지는 다음에 계속 서술

Header

디코딩하면 type(언제나 JWT가 들어감)과 alg(알고리즘)정보가 나온다. 여기는 ‘3번 서명값’을 만드는데 사용될 알고리즘이 지정된다(ex : HS256)

즉, Header와 Payload 그리고 서버에 감춰둔 비밀 값 이라는 3가지 요소를 암호화 알고리즘에 넣고 돌리면 3번 서명값이 나오는 거다. 암호화 알고리즘은 한쪽 방향으로만 계산이 돼서 토큰을 탈취해서 염병을 해도 서버가 감춰둔 비밀 값을 알아낼 수 없다.

여기서 주목. 서버는 요청에 토큰 값이 실려오면 Header와 Payload를 서버가 감춘 비밀값과 함께 알고리즘을 돌려서 계산된 값이 3번 서명값과 같다면, 그리고 유효기간이 지나지 않았다면 인가처리를 해준다. 이것이 JWT다. 참고로 Payload가 조금이라도 바뀌면 알고리즘 돌려서 나오는 값이 완전히 달라져서 3번 서명값과 같아지지 않는다. 이를 통해 payload변조를 알아챌 수 있다.

이를 통해, 서버는 사용자들의 상태를 어디다가 기록해둘 필요 없이 자신의 비밀 값만 가지고 있다면 요청들 들어올 때마다 헤비한 작업을 할 필요가 없다. 이처럼 시간에 따라 바뀌는 어떠한 상태값을 갖지 않는 걸 stateless하다고 표현한다. 세션은 반대로 stateful하다고 표현할 수 있다

그러나 JWT의 결점

stateful방식 즉 모든 사용자들의 상태를 기억하는 건 구현이 어렵고 고려사항도 많지만 구현만 되면 기억하는 대상의 상태들을 언제든 제어가능하다. 예를 들어 한 기기에서만 로그인가능한 서비스를 만든다고 하자. PC에서 로그인한 상태의 어떤 유저가 핸드폰으로 또 로그인하려하면 PC에서는 로그아웃되도록 만들 수도 있다는 얘기! JWT는 이런게 불가능하다. 이미 줘버린 토큰을 뺏을 수도 없고 토큰의 발급 내역(payload에 저장되는 정보)를 서버가 따로 추적할 수도 없으니..

즉 JWT는 내가 쥐고 있을 필요가 없어 편하긴 한데 그만큼 통제를 못 하는 방식이다.

또한 토큰이 만약 탈취된다면, 이 토큰을 무효화할 방법이 없다는 것도 큰 단점이다ㅋㅋ. 때문에 토큰을 두 개 발급하는 방법이 쓰이곤 한다. 완벽한 해결책은 아님. 이런 문제들이 큰 이슈가 안되는 서비스들에 한해서만 JWT만을 쓰는 인가방식을 쓰는 게 좋다.

코드를 짤 때 예를 들어 user의 최대 인원이 10명이라고 하면 그 인원이 쓰이는 곳에 전부다 10이란 숫자를 때려받는 방법이 있다. 이렇게 데이터를 직접 코드에 때려박는 걸 하드코딩이라고 표현한다. 이 방식의 문제점은, 만약 user의 최대인원이 20명으로 늘어나면 하드코딩된 곳 하나하나를 찾아 20으로 바꿔줘야 한다. 그러나 MAX_USER라는 변수를 사용한다면, MAX_USER의 값을 바꿔주는 것 하나만으로 방금 전 겪을 뻔한 뻘짓들을 안 할 수 있게 된다.

 

CSS에서도 여러 가지 컴포넌트에 공통적으로 들어가는 css값들이 있다. rgb방식으로 표현하는 특정 컬러라던지..이런 것들을 하나하나 직접 하드코딩하기 싫다면, CSS에서도 변수같이 쓸 수 있는 기능이 있다. 그 방법을 소개한다.

 


:root 가상클래스를 활용해 'CSS변수'를 다룰 수 있다.

 

CSS세계에서 가상클래스란 id선택자(#)나 클래스선택자(.)로 집어낼 수 없는 요소들을 선택하는 선택자로, 요소의 속성값, 상태, 상대적인 위치등을 이용해 특정 요소를 집어낼 수 있다. 

ex) :hover, :active, :nth-child(n) ...

 

이 때 :root 가상 클래스는 문서 트리의 루트 요소를 선택하는데 HTML의 루트 요소는 <html>이므로 :root로 집어낸 놈은 <html>과 같다. 단 가상클래스 선택자의 우선순위가 태그 선택자의 우선순위보다 더 높다. 이를 통해 :root로 최상위 요소에 변수를 선언하면 모든 요소에서 이 변수를 쓸 수 있는 것.

 

문서에서 공통으로 사용될 속성을 미리 변수에 선언하는 것이라 보면 된다. 하이픈(-) 두개를 통해 속성이름을 지정하고, 콜론뒤에 값을 지정하면 된다. 마치 key-value처럼..

 

 

사용할 때는 var로 변수를 선언하고 소괄호 안에 속에 key값을 입력하면 된다.

 

+ Recent posts