* 미들웨어 구현
- 미들웨어 타입은 reqHandler 또는 ErrorHandler가 가능.
- ## type, inteface 차이점 쉬운맛
1) 원시성 데이터 사용
type: 가능, interface: 불가능
2) 튜플 사용
type: 가능, interface: 불가능
3) interface 취약점
중복 선언하면 동일 실행 환경에서는
모두 합쳐지는 특징이 있어서
의도하지 않고 다른개발자가 같은 이름이 있는
인퍼페이스를 선언하면 에러 메세지가 뜨지 않는다
결론 가능하면 type을 더 많이 쓰는것이 좋다.
버전이 올라가면서 interface가 성능이 조금더 좋다는
이슈가 사라졌다. 고로 type을 조금더 선호하자
/**
* Represents an HTTP request handler function.
*/
type RequestHandler = (req: Request, res: Response, next: (err?: Error) => void) => void;
/**
* Represents an error handling middleware function.
*/
type ErrorHandler = (err: Error, req: Request, res: Response, next: (err?: Error) => void) => void;
/**
* Represents a middleware function, which can be either a RequestHandler or an ErrorHandler.
*/
type Middleware = RequestHandler | ErrorHandler;
- 서버에 middlewares 배열을두고
class Server {
private server: net.Server;
private router?: Router;
private staticDirectory?: string;
private middlewares: Middleware[] = [];
- 길이만큼 돌려준다.
- 에러가 있으면 에러도 같이 넘겨준다.
- 에러가 없으면, 등록된 미들웨어 실행
private runMiddleware(
middlewares: Middleware[],
index: number,
err: Error | null,
req: Request,
res: Response,
): void {
if (index < 0 || index >= middlewares.length) return;
const nextMiddleware = middlewares[index];
const next = (e?: Error) => this.runMiddleware(middlewares, index + 1, e || null, req, res);
if (err) {
// 에러가 있고, 다음에 실행할 미들웨어가 에러 처리기인경우 에러처리 미들웨어 실행
if (this.isErrorHandler(nextMiddleware)) {
(nextMiddleware as ErrorHandler)(err, req, res, next);
} else {
// 에러가 있고, 다음에 실행할 미들웨어가 에러 처리기가 아니면 그 다음 미들웨어를 찾는다
this.runMiddleware(middlewares, index + 1, err, req, res);
}
} else {
/**
* as로 타입강제지정, 등록된 함수 실행
* 아래코드와 같은기능
* if (this.isRequestHandler(nextMiddleware)) {
* nextMiddleware(req, res, next);
*/
(nextMiddleware as RequestHandler)(req, res, next);
}
}
- test용 로거 미들웨어
export const logger = (req : Request, res : Response, next) => {
const uid = randomUUID();
console.log(`[${uid}] [${req.method}] [${req.path}]`);
next();
}
* logger color 구현
import {Request} from "../was/request";
import {Response} from "../was/response";
import {randomUUID} from "node:crypto";
const colors = {
green: '\x1b[32m',
cyan: '\x1b[36m',
red: '\x1b[31m',
yellow: '\x1b[33m',
reset: '\x1b[0m',
}
const methodColorMap = {
get: colors.green,
post: colors.cyan,
put: colors.yellow,
delete: colors.red
}
export const logger = (req : Request, res : Response, next) => {
const coloredMethod = method => {
return `${methodColorMap[method.toLowerCase()]}${method}${colors.reset}`
}
const uid = randomUUID();
console.log(`[${uid}] [${coloredMethod(req.method)}] [${req.path}]`);
next();
}
* 미들웨어 개선 router -> skip
- 정보가 많은 스프링쪽을 따라서 구현하고자함.
// 기본 미들웨어 타입 정의
type BaseMiddleware = RequestHandler | ErrorHandler;
// 확장된 미들웨어 인터페이스
interface MiddlewareExtension {
__path?: string;
}
// 최종 미들웨어 타입 (교차 타입 사용)
type Middleware = BaseMiddleware & MiddlewareExtension;
/**
* 인자1개 -> 첫번째인자를 Middleware 취급
* 인자 2개 -> 첫번째인자는 path, 두번째인자는 middleware
* @param path
* @param fn
*/
public use (path: string | Middleware, middleware?: Middleware) {
if (typeof(path) === "string" && middleware) {
middleware.__path = path;
} else if (typeof path === "function") {
middleware = path;
} else {
throw new Error("Usage: use(path, fn) or use(fn)");
}
this.middlewares.push(middleware);
};
* 프론트 컨트롤러 도입 v1
- 설계
- 인터페이스 설계
import {Request} from "../../was/request";
import {Response} from "../../was/response";
export interface ControllerV1 {
process(req : Request , res : Response) : void
}
- 회원가입 컨트롤러
- viewPath를 받아서, 해당하는 html파일을 render 해준다.
import {ControllerV1} from "../ControllerV1";
import {Request} from "../../../was/request";
import {Response} from "../../../was/response";
import * as path from "path";
export class MemberFormControllerV1 implements ControllerV1{
public async process(req : Request , res : Response) : Promise<void>{
const viewPath : string = path.join(process.cwd(), 'dist','views','new-form.html');
await res.forward(req,res,viewPath);
}
}
- forward 함수 (기존의 sendfile과 유사)
public async forward(req: Request, res: Response, viewPath : string): Promise<void> {
try {
const stats = await stat(viewPath);
const file = await fs.promises.readFile(viewPath);
const ext = path.extname(viewPath).slice(1);
const mimeType = COMMON_MIME_TYPES[ext] || 'application/octet-stream';
const maxAge = cachePolicy[ext] || 'public, max-age=3600';
res.setCacheControl(maxAge);
// ETag 생성
const etag : string = createHash('md5').update(file).digest('hex');
// Cache-Control, ETag, Last-Modified 설정
res.header('ETag', `${etag}`)
.header('Last-Modified', stats.mtime.toUTCString());
// 조건부 요청 처리
if (req.headers.get('if-none-match') === etag) {
res.header('Content-Type', mimeType).status(304).send();
return;
}
// 내용이 바뀐경우에만, 새로운 body를 보내준다.
res.render(file,mimeType);
} catch (error) {
console.error('File read error:', error);
res.status(404).send('File Not Found');
}
}
- 프론트 컨트롤러 (핵심)
- 맵에 < url , 컨트롤러>를 담는다.
- reqURI에 맞춰서 해당하는 컨트롤러의 process를 실행한다.
import {ControllerV1} from "./ControllerV1";
import {MemberFormControllerV1} from "./controller/MemberFormControllerV1";
import {Response} from "../../was/response";
import {Request} from "../../was/request";
export class FrontControllerServletV1 {
private readonly urlPatterns:string;
private controllerMap : Map<String,ControllerV1> = new Map<String, ControllerV1>
constructor() {
this.urlPatterns = "/front-controller/v1/";
this.controllerMap.set(this.urlPatterns+"members/new-form", new MemberFormControllerV1());
}
public service(req : Request, res : Response){
console.log('FrontControllerV1.service');
const reqURI : string = req.path;
const controller = this.controllerMap.get(reqURI);
if(!controller){
res.status(404).send();
return;
}
controller.process(req,res);
}
}
* static serving fix
- 참고 : favicon은 req.url = "/favicon"으로 요청한다.
- 문제 : 프론트 컨트롤러 도입후 favicon, html 내부의 js에 대한 응답이 없음을 발견
- 해결 : 미들웨어 도입
import {Request} from "../was/request";
import {Response} from "../was/response";
import * as path from "node:path";
import * as fs from "node:fs";
export const staticServe = (req : Request, res : Response) => {
const mimeType = {
".ico": "image/x-icon",
".html": "text/html",
".js": "text/javascript",
".css": "text/css",
".png": "image/png",
".jpg": "image/jpeg",
".eot": "appliaction/vnd.ms-fontobject",
".ttf": "aplication/font-sfnt",
}
const ext = path.parse(req.path).ext
const publicPath = path.join(process.cwd(), 'dist','views');
if (Object.keys(mimeType).includes(ext)) {
fs.readFile(`${publicPath}${req.path}`, (err, data) => {
if (err) {
res.status(404).send();
return;
}
res.status(200).render(data);
})
return;
}
}
async function main() {
const app = new Server();
app.use(logger);
app.use(staticServe);
- server.ts 수정
- 미들웨어 돌린후 프론트 컨트롤러 실행 (추후 개선 필요)
private async handleRequest(headers: string, body: Buffer, socket: net.Socket): Promise<void> {
const req = new Request(headers, body);
const res = new Response(socket);
this.runMiddleware(this.middlewares, 0, null, req, res);
const frontControllerServletV1 = new FrontControllerServletV1();
if(req.path.startsWith("/front-controller/v1"))
frontControllerServletV1.service(req,res);
}
* memberListController 추가
- 하려고 봤더니
- 동적 렌더링 구현이 먼저다.
- 일단 정적 html으로 test 한다.
- 프론트 컨트롤러 맵에 < url, controller >추가
export class FrontControllerServletV1 {
private readonly urlPatterns:string;
private controllerMap : Map<String,ControllerV1> = new Map<String, ControllerV1>
constructor() {
this.urlPatterns = "/front-controller/v1/";
this.controllerMap.set(this.urlPatterns+"members/new-form", new MemberFormControllerV1());
this.controllerMap.set(this.urlPatterns+"members", new MemberListControllerV1());
}
- member리스트 컨트롤러 구현
import {ControllerV1} from "../ControllerV1";
import {Request} from "../../../was/request";
import {Response} from "../../../was/response";
import * as path from "path";
export class MemberListControllerV1 implements ControllerV1{
public async process(req : Request , res : Response) : Promise<void>{
const viewPath : string = path.join(process.cwd(), 'dist','views','members.html');
await res.forward(req,res,viewPath);
}
}
* 동적렌더링 구현
- gpt 답변
- 추정 : 용빼는 재주가 있는것은 아니고, ejs 내의 <% %> 등을 조작해서, 안에 data를 직접 집어넣는 방식으로 추정.
/**
* 간소화된 렌더 함수
* @param template 템플릿 파일 이름 (확장자 제외)
* @param data 템플릿에 전달할 데이터 객체
* @returns 렌더링된 HTML 문자열
*/
function render(template: string, data: Record<string, any>): string {
const viewsFolder = path.join(process.cwd(), 'views');
const filePath = path.join(viewsFolder, `${template}.ejs`);
let content = fs.readFileSync(filePath, 'utf8');
// <%= ... %> 형식의 표현식 처리
content = content.replace(/<%=\s*(.+?)\s*%>/g, (match, p1) => {
const value = evaluateExpression(p1, data);
return escapeHtml(String(value));
});
// <% ... %> 형식의 JavaScript 코드 처리
content = content.replace(/<%([\s\S]+?)%>/g, (match, p1) => {
return evaluateCode(p1, data);
});
return content;
}
function evaluateExpression(expression: string, data: Record<string, any>): any {
const keys = Object.keys(data);
const values = Object.values(data);
const func = new Function(...keys, `return ${expression};`);
return func(...values);
}
function evaluateCode(code: string, data: Record<string, any>): string {
const keys = Object.keys(data);
const values = Object.values(data);
const func = new Function(...keys, `
let __output = '';
const __append = (s) => __output += s;
${code}
return __output;
`);
return func(...values);
}
function escapeHtml(str: string): string {
const escapeChars: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return str.replace(/[&<>"']/g, match => escapeChars[match]);
}
- 찾아보니 ejs 모듈을 사용해서 render하면 된다.
import * as ejs from 'ejs';
import * as fs from 'fs';
import * as path from 'path';
interface Member {
id: number;
loginId: string;
name: string;
}
interface PageData {
title: string;
members: Member[];
}
function renderEjsTemplate(templatePath: string, data: PageData): string {
// 템플릿 파일 읽기
const template = fs.readFileSync(templatePath, 'utf-8');
// EJS를 사용하여 템플릿 렌더링
const html = ejs.render(template, data, {
filename: templatePath // 이를 통해 include 기능 등을 사용할 수 있습니다.
});
return html;
}
// 사용 예시
const pageData: PageData = {
title: '회원 목록',
members: [
{ id: 1, loginId: 'user1', name: '홍길동' },
{ id: 2, loginId: 'user2', name: '김철수' },
{ id: 3, loginId: 'user3', name: '이영희' }
]
};
const templatePath = path.join(__dirname, 'views', 'members.ejs');
const renderedHtml = renderEjsTemplate(templatePath, pageData);
console.log(renderedHtml);
export { renderEjsTemplate };
* 프론트 컨트롤러 V2 구현
- 문제 : 모든 컨트롤러에 뷰로 이동하는 부분의 코드가 중복
- 설계
- 인터페이스 설계
- 리턴타입만 MyView로 변경
import {Request} from "../../was/request";
import {Response} from "../../was/response";
import {MyView} from "../MyView";
export interface ControllerV2 {
process(req : Request , res : Response) : MyView
}
- MyView는 render를 담당한다.
- todo : 동적렌더링
- html 인경우 -> 그냥반환
- ejs인경우 -> 동적렌더링후 html 반환
- todo : 동적렌더링
import {Response} from "../was/response";
import {Request} from "../was/request";
export class MyView {
private readonly viewPath : string;
constructor(viewPath : string) {
this.viewPath = viewPath;
}
public async render(req : Request, res : Response) : Promise<void> {
await res.forward(req,res,this.viewPath);
}
}
public process(req : Request , res : Response) : MyView{
const viewPath : string = path.join(process.cwd(), 'dist','views','new-form.html');
return new MyView(viewPath);
}
- 프론트 컨트롤러 v2
- view를 return 하도록 수정
import {Response} from "../../was/response";
import {Request} from "../../was/request";
import {ControllerV2} from "./ControllerV2";
import {MemberFormControllerV2} from "./controller/MemberFormControllerV2";
import {MemberListControllerV2} from "./controller/MemberListControllerV2";
export class FrontControllerServletV2 {
private readonly urlPatterns:string;
private controllerMap : Map<String,ControllerV2> = new Map<String, ControllerV2>
constructor() {
this.urlPatterns = "/front-controller/v2/";
this.controllerMap.set(this.urlPatterns+"members/new-form", new MemberFormControllerV2());
this.controllerMap.set(this.urlPatterns+"members", new MemberListControllerV2());
}
public service(req : Request, res : Response){
const reqURI : string = req.path;
if(!this.controllerMap.has(reqURI)){
res.status(404).send();
return;
}
const controller : ControllerV2= this.controllerMap.get(reqURI);
const view = controller.process(req,res);
view.render(req,res);
}
}
'JS > boostCamp' 카테고리의 다른 글
24.10.5~7. 개발일지 // 회원가입구현, redirect, cookie, session, 인증 (0) | 2024.10.07 |
---|---|
24. 10. 4. 개발일지 // 정적서빙버그 fix (2) | 2024.10.05 |
24.10.3. 개발일지 // MemberSaveController 구현, 프론트컨트롤러 v4구현, 유연한 컨트롤러 구현, 어댑터패턴, instanceof interface (0) | 2024.10.03 |
24. 10. 2. 개발일지 // 프론트컨트롤러 v3, 동적렌더링, mapToObj (2) | 2024.10.02 |
24.9.30. 개발일지 // ts설정, favicon, view to dist, 조건부 요청 (0) | 2024.09.30 |