본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다
패스트캠퍼스 환급챌린지란?
(~6/20) 50일의 기적 AI 환급반💫 | 패스트캠퍼스
초간단 미션! 하루 20분 공부하고 수강료 전액 환급에 AI 스킬 장착까지!
fastcampus.co.kr
https://fastcampus.info/4n8ztzq
야근으로 엄청 피곤하지만 재미있게 강의를 들을 수 있게 해주는 환급챌린지 최공!
30일차 공부 시작!

오늘의 강의 주제는 '로그인-병렬, 연속적 비동기 API 처리의 이해'이다.

(오늘도 마찬가지로 모자이크 처리..)
오늘의 학습 내용
* 로그인과 토큰 인증 흐름 이해
우선 로그인 API는 사용자의 아이디와 비밀번호를 입력받아 인증 결과를 반환한다. 이때 실제로는 데이터베이스를 통한 사용자의 정보 검사 과정이 필요하지만, 이번 실습에서는 무작위로 로그인 성공 여부를 결정하는 방식으로 간단하게 처리한다.
성공적인 로그인 이후 클라이언트에게는 사용자 ID와 함께 고유한 토큰 값이 전달된다. 이때 전달되는 토큰은 이후의 모든 API 요청에서 인증을 대신하는 키 역할을 한다. HTTP는 상태가 유지되지 않는 프로토콜이기 때문에 클라이언트가 서버에 인증 상태를 유지하려면, 모든 요청마다 토큰 값을 함께 보내야 한다. 이러한 방식이 바로 토큰 기반 인증이다.
* axios와 비동기 API 호출 방식
API 호출에는 JavaScript의 fetch API가 아닌 axios 라이브러리를 사용했다. 실제 현업에서는 axios의 사용성이 더 우수하다는 이유로 많이 활용된다. 설정이 간단하며 헤더 삽입이나 응답 처리 등에서 코드가 간결하게 작성되기 때문이다.
로그인 성공 후, 서버로부터 받은 사용자 ID와 토큰을 활용해 두 가지 API(`/api/user/:id`, `/api/user/:id/posts`)를 호출해야 했는데, 이 과정은 병렬적으로 처리하는 것이 좋다. 프로필 정보와 게시물 목록을 순차적으로 받아올 경우 전체 응답 시간이 길어질 수 있기 때문이다.
이를 병렬적으로 처리하기 위해 axios.all()을 사용했다. 이는 Promise.all과 동일한 개념으로, 여러 요청을 동시에 보내고 결과를 묶어서 받는다. 이 방식은 동시에 실행 가능한 비동기 요청에 적합하며, 두 요청 중 하나라도 실패하면 오류 처리를 통해 적절한 피드백을 줄 수 있다.
* 토큰 검사와 라우팅 처리
서버의 각 API에서는 요청 헤더에 토큰이 존재하는지를 검사하는 로직이 포함되어 있다. 토큰이 없을 경우 서버는 403 에러를 반환하여 인증되지 않은 요청임을 알린다. 이렇게 헤더를 통해 인증 정보를 검증하는 방식은 보안상 중요한 역할을 하며, 사용자가 로그인한 상태여야만 특정 API를 사용할 수 있도록 하는 제한을 구현한다.
응답을 받은 후에는 클라이언트 스토어(예: `this.#data.store`)에 사용자 프로필과 글 목록을 저장하고, 클라이언트 사이드 라우팅을 이용해 사용자 프로필 페이지로 이동한다.
* 공통 로직 분리와 컴포넌트 구조
반복적으로 사용되는 컴포넌트를 별도의 클래스(TextField)로 추상화하고, 로그인 뷰에서는 이를 조합하여 구성했다. 또한 중복되는 로직이나 데이터 초기화 과정은 초기화 메서드로 분리되어 코드의 재사용성과 유지보수성이 높아졌다. 데이터도 외부에서 주입받는 구조로 설계되어 있어 상태 의존도를 낮추고 데이터 관리가 명확하게 이루어진다.
공통 요소가 많은 페이지는 핵심 로직을 공유할 수 있도록 베이스 클래스 형태로 만들고, 페이지별 특화된 기능만 독립적으로 작성하는 구조가 유지된다면, 전체 코드의 품질과 생산성이 올라갈 수 있다.
* 실습 코드 - 서버
const path = require('path');
const express = require('express');
const morgan = require('morgan');
const { urlencoded, json } = require('body-parser');
const { v4 } = require('uuid');
const axios = require('axios');
const app = express();
app.use(express.static('dist'));
app.use(morgan('dev'));
app.use(urlencoded({ extended: false }))
app.use(json())
app.get('/api/user/:id', (req, res) => {
if (!req.headers['token']) {
return res.status(403).send({
status: 'Error'
});
}
axios.get('https://randomuser.me/api')
.then(result => {
const [userProfile] = result.data.results;
const { name, picture, phone, email, country } = userProfile;
res.status(200).send({
status: 'OK',
result: {
name, picture, phone, email, country
}
})
})
.catch(e => {
res.status(400).send({
status: 'Error'
});
});
});
app.get('/api/user/:id/posts', (req, res) => {
if (!req.headers['token']) {
return res.status(403).send({
status: 'Error'
});
}
axios.get(`https://jsonplaceholder.typicode.com/posts?userId=${req.params.id}`)
.then(result => {
res.status(200).send({
status: 'OK',
results: result.data
})
})
.catch(e => {
res.status(400).send({
status: 'Error'
});
});
});
app.post('/api/authentication', (req, res) => {
if (Math.floor(Math.random() * 10) % 2 === 0) {
res.status(200).send({
status: 'OK',
result: {
id: Math.floor(Math.random()*10),
token: v4(),
}
});
} else {
res.status(400).send({
status: 'Error'
});
}
});
app.listen(8080, () => {
console.log('ready to dumy signup server');
});
* 실습 코드 - 클라이언트 axios
import template from './login.template';
import TextField from '../views/text-field';
import axios from 'axios';
export default class Login {
#template = template;
#data;
#container;
#loginFail = false;
#fields = [];
constructor(container, data) {
this.#container = document.querySelector(container);
this.#data = data;
this.#initialize();
}
#initialize = () => {
const idField = new TextField('#login-fields', {
id: 'userid', label: '아이디', type: 'text', placeholder: '아이디를 입력해주세요', require: true,
});
const passwordField = new TextField('#login-fields', {
id: 'password', label: '비밀번호', type: 'password', placeholder: '**********', require: true,
});
this.#fields.push(idField);
this.#fields.push(passwordField);
}
#onSubmit = e => {
e.preventDefault();
const loginData = this.#fields
.map(field => ({ [field.name]: field.value }))
.reduce((a, b) => ({ ...a, ...b }), {});
axios.post('/api/authentication', loginData)
.then(result => {
return result.data.result;
})
.then(({ id, token }) => {
const options = { headers: { token } };
this.#data.store.token = token;
return axios.all([
axios.get(`/api/user/${id}`, options),
axios.get(`/api/user/${id}/posts`, options),
]);
})
.then(([profile, posts]) => {
this.#data.store.userProfile = profile.data.result;
this.#data.store.userPosts = posts.data.results;
location.href = '/#/profile';
})
.catch(error => {
this.#loginFail = true;
this.render();
});
}
render = () => {
this.#container.innerHTML = this.#template({ ...this.#data, loginFail: this.#loginFail });
this.#fields.forEach(field => {
field.render(true);
});
this.#container.addEventListener('submit', this.#onSubmit);
}
}




마치며
오늘도 환급 챌린지 32강 공부 완료!!
이번 강의에서는 로그인 처리를 중심으로, 연속적이거나 병렬적으로 동작하는 비동기 API 호출 방식에 대해 실습을 통해 익혀보았다. Node.js를 이용한 간단한 서버와 프론트엔드 코드 작성이 주요했다.