Open
Conversation
Contributor
|
키워드가 전반적으로 잘 정리되어 있어서 여러 개념을 살펴볼 수 있었습니다~ |
seoki180
reviewed
Apr 13, 2026
| - 단점 | ||
| - 모듈을 불러올 때 동기적으로 작동하기에, 서버 사이드에서는 문제가 적지만, 브라우저 환경에서는 로딩이 완료될 때까지 시행이 멈춰 성능 저하 가능 | ||
| - 어떤 모듈을 쓰지 않는지 동적 시간에 알기 때문에 빌드에 사용하지 않는 코드를 제거하는 트리 쉐이킹이 어렵다. | ||
| - 자바스크립트 공식 표준인 ESModule이 등장하여 이제는 레거시가 될 가능성 |
Collaborator
There was a problem hiding this comment.
물론 JS표준은 ESM이고 현대적인 장점덕분에 ESM을 많이 쓰지만 그렇다고 CJS가 완전히 레거시가 되진않습니다!
조건부, 동적 require이 필요한경우,
동기 로딩으로 초기화를 해야할떄 ,
이미 존재하는 생태계 적용 등의 이유로
의도적으로 CJS를 쓰는 경우도 있고,
require 함수가 필요해 ESM에서 이를 가져다 쓰는 경우도 있다고 하네요
또 라이브러리를 만들때 CJS, ESM 모두 지원하는 경우가 많다고 하네요
https://toss.tech/article/commonjs-esm-exports-field
|
|
||
| 단점 | ||
|
|
||
| - node.js에서 사용하려면 package.json 설정 필요, 기존 CJS 라이브러리 불러올 때 가끔 문제 생길 수 있음 |
Collaborator
There was a problem hiding this comment.
또한 TS 프로젝트에서 ESM을 쓰면 TS파일인데도 import 확장자를 .js로 써야하는 경우도 있어요,,
Comment on lines
+87
to
+110
| # let, const / var 차이 | ||
|
|
||
| var 방식은 스코프가 모호해 예기치 못한 오류 발생 가능 | ||
|
|
||
| - const : 재할당 불가 상수 | ||
| - let : 재할당 가능 변수 | ||
|
|
||
| ### var 쓰면 varbo | ||
|
|
||
| ```jsx | ||
| var name ='Kim'; | ||
| var name ='Lee'; // 가능 | ||
|
|
||
| let age =20; | ||
| let age =30; // 에러 | ||
| ``` | ||
|
|
||
| ```jsx | ||
| if (true) { | ||
| var hello = 'hi'; | ||
| let bye = 'see ya'; | ||
| } | ||
| console.log(hello); // 'hi' 출력됨 | ||
| console.log(bye); // 에러! (bye is not defined) |
Collaborator
There was a problem hiding this comment.
둘의 차이를 이해할때 호이스팅(Hoisting)을 이해하는게 중요합니다!
Comment on lines
+255
to
+641
| - 프로젝트 아키텍쳐가 무엇인지, 왜 중요한지 설명해주세요 | ||
|
|
||
| # 레이어드 아키텍쳐가 무엇인지, 각 계층(Controller / Service / Repository)의 역할을 설명 | ||
|
|
||
| 레이어드 아키텍쳐란? | ||
|
|
||
| 소프트웨어를 구성하는 요소들을 비슷한 관심사로 층을 나누어 쌓아 올린 구조. | ||
|
|
||
| 각 계층은 바로 아래 계층에만 의존하고, 이를 통해 코드 가독성과 유지보수성을 높인다. | ||
|
|
||
|  | ||
|
|
||
| 컨트롤러 | ||
|
|
||
| - 어플리케이션 맨 앞단에서 사용자 요청을 받고 응답을 반환하는 창구 | ||
| - http 요청을 해석하고, 비즈니스 로직을 호출한 뒤 적절한 형식(json 등)으로 사용자에게 전달한다. | ||
|
|
||
| 서비스 (비즈니스 로직) | ||
|
|
||
| - 핵심 비즈니스 로직을 수행하는 곳 | ||
| - 데이터 유효성 검사하거나, 여러 repository를 조합하여 기능을 처리하는 곳 | ||
|
|
||
| 레포지토리 | ||
|
|
||
| - 데이터베이스와 통신을 담당하는 곳 | ||
| - 디비에 접근해 sql 실행하거나 엔티티 객체를 관리 | ||
| - 로직은 거의 포함하지 않고, 데이터 출입에만 집중 | ||
|
|
||
| 계층을 나누는 이점 | ||
|
|
||
| - 코드의 재사용성이 높아진다. | ||
| - 테스트 용이성 | ||
| - DB를 직접 연결하지 않아도, 서비스 계층만 떼어서, 논리적으로 맞는지 테스트하기에 좋음 | ||
| - 유지보수 편의성 | ||
| - mysql → mongodb 전환 시 계층 분리와 의존성이 나눠져있다면 repository 계층만 수정하면 됨 | ||
|
|
||
| # 아키텍쳐 | ||
|
|
||
| 아키텍쳐? | ||
|
|
||
| - 어떤 대상의 구성과 동작 원리, 구성 요소간의 관계 및 시스템 외부 환경과의 관계를 설명하는 설명서 | ||
| - 시스템의 설계 및 동작하는 방식 | ||
|
|
||
|  | ||
|
|
||
| 기능도 중요하지만, 구조도 중요하다. | ||
|
|
||
| → 초기에는 개발 비용이 많이 들지만, 장기적으로 보았을 때 유지보수에 드는 비용이 증가하기 때문 | ||
|
|
||
| 좋은 구조를 만드는 방법에는 여러가지가 있다. | ||
|
|
||
| - 패러다임 | ||
| - 설계원칙 SOLID | ||
| - 응집원칙 | ||
| - 결합원칙 등… | ||
|
|
||
| → 모두 지키기 어려움 | ||
|
|
||
| 아키텍쳐 패턴은 이러한 원칙들을 쉽게 지킬 수 있도록 도와줌. 따라하는 것만으로도 원칙 일부를 지키게 해준다. | ||
|
|
||
| 헥사고날 아키테쳐 | ||
|
|
||
|  | ||
|
|
||
| 기존 레이어드 아키텍쳐를 헥사고날 아키텍쳐로 바꿔보자. | ||
|
|
||
| 아래와 같은 경우, 특정 클래스에 의존하게된다. | ||
|
|
||
| ```tsx | ||
| class UserService { | ||
| constructor(private repository: UserRepository) {} // 특정 클래스 의존 | ||
|
|
||
| async save(data) { | ||
| return this.repository.save(data); // 리포지토리 함수 직접 호출 | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| 헥사고날 아키텍쳐로 변환하면, 서비스와 레포지토리 사이에 벽(port)을 세워 서비스에서는 레포지토리가 어떤 것인지 모르고, 설명서만 보고 호출할 수 있게 된다. | ||
|
|
||
| ```tsx | ||
| interface IUserRepository { | ||
| save(data): Promise<void>; | ||
| } | ||
|
|
||
| // 서비스는 이 인터페이스만 바라보기 때문에 | ||
| // 진짜 리포지토리가 누군지 모름 | ||
| class UserService { | ||
| constructor(private repository: IUserRepository) {} // 인터페이스 의존 | ||
| } | ||
| ``` | ||
|
|
||
| 이제 헥사고날 파일 구조까지 완전히 분리해보자. | ||
|
|
||
| 우선 외부 기술로부터 독립된 애플리케이션의 순수 비즈니스 영역이다. | ||
|
|
||
| 하나는 서비스가 DB에 저장하기 위해 밖으로 요청하는 포트, | ||
|
|
||
| ```tsx | ||
| // src/application/ports/out/user-repository.port.ts | ||
| export interface UserRepositoryPort { | ||
| save(user: any): Promise<void>; | ||
| } | ||
| ``` | ||
|
|
||
| 하나는 컨트롤러가 서비스에 요청할 기능을 인터페이스로 정의한다. | ||
|
|
||
| ```tsx | ||
| // src/application/ports/in/create-user.use-case.ts | ||
| export interface CreateUserUseCase { | ||
| execute(name:string, email:string): Promise<void>; | ||
| } | ||
| ``` | ||
|
|
||
| 이렇게 안, 밖과 연결할 수 있는 포트를 만들었다면 이 포트를 사용하는 순수 비즈니스 로직만 담겨있는 서비스 코드를 작성한다. | ||
|
|
||
| ```tsx | ||
| // src/application/services/user.service.ts | ||
| export class UserService implements CreateUserUseCase { | ||
| constructor(private readonly userRepository: UserRepositoryPort) {} // 아웃포트 주입 | ||
|
|
||
| async execute(name: string, email: string): Promise<void> { | ||
| console.log('비즈니스 로직 실행 중...'); | ||
| const user = { name, email, createdAt: new Date() }; | ||
| await this.userRepository.save(user); // 실제 저장은 어댑터가 알아서 함 | ||
| } | ||
| } | ||
|
|
||
| ``` | ||
|
|
||
| 즉 외부와 연결할 포트, 그리고 그 포트를 이용하는 서비스 코드로 이뤄졌다. | ||
|
|
||
| 이제 실제 DB와 통신하거나 HTTP 요청을 받는 영역을 구현해보자. | ||
|
|
||
| 이 부분은 위에서 정의한 interface를 구현하는 클래스를 각자의 외부 기술로 구현하면 된다. | ||
|
|
||
| ```tsx | ||
| // src/infrastructure/adapters/persistence/typeorm-user.repository.ts | ||
| export class TypeOrmUserRepository implements UserRepositoryPort { | ||
| async save(user: any): Promise<void> { | ||
| console.log('TypeORM을 사용하여 DB에 저장 완료!'); | ||
| // return await this.orm.save(user); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ```tsx | ||
| // src/infrastructure/adapters/web/user.controller.ts | ||
| export class UserController { | ||
| constructor(private readonly createUserUseCase: CreateUserUseCase) {} | ||
|
|
||
| async handleRequest(req: any, res: any) { | ||
| const { name, email } = req.body; | ||
| await this.createUserUseCase.execute(name, email); | ||
| res.status(201).send('User Created'); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| # 디자인 패턴 | ||
|
|
||
| node.js에서 express와 같이 유용하게 사용할 수 있는 패턴들 위주로 정리해습니다. | ||
|
|
||
| ### 미들웨어를 통한 책임 연쇄 패턴 | ||
|
|
||
| 미들웨어 : 요청과 응답 사이에서 작업을 수행하고 next()로 다음 순서에 넘기는 방식 | ||
|
|
||
| ```tsx | ||
| const express = require('express'); | ||
| const app = express(); | ||
|
|
||
| // 로깅 미들웨어 (모든 요청에서 실행) | ||
| const logger = (req, res, next) => { | ||
| console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`); | ||
| next(); // 다음 미들웨어나 라우터로 제어권을 넘김 | ||
| }; | ||
|
|
||
| app.use(logger); | ||
|
|
||
| app.get('/', (req, res) => { | ||
| res.send('Hello World'); | ||
| }); | ||
| ``` | ||
|
|
||
| ### 전략 패턴 (Strategy) - passport.js 예시 | ||
|
|
||
| 실행 중에 알고리즘(행위)을 동적으로 교체할 수 있도록 설계된 디자인 패턴이다. | ||
|
|
||
| 게임을 만들 때 캐릭텨가 어떤 무기를 들고 있냐에 따라 공격 방식이 달라진다. | ||
|
|
||
| 즉 function이 자주 바뀔 수 있다. | ||
|
|
||
| 전략패턴에서는 “공격하기”라는 행동을 정의하고, 구체적인 전략들은 그것을 상속받아 디테일한 것을 구현하는 방식이다. | ||
|
|
||
| 전략 패턴을 쓰지 않았다면, attack 함수에 아래와 같이 분기를 해야한다. | ||
|
|
||
| ```tsx | ||
| if (무기 == "검") { | ||
| ... | ||
| } | ||
| else if (무기 == "활") { | ||
| ... | ||
| } | ||
| ... | ||
| ``` | ||
|
|
||
| 즉, 원래 attack 함수는 “공격을 실행한다”는 명령만 내리면 되는데 위와 같이 if-else로 모든 공격 방법을 다 알고있어야 하게 된다. | ||
|
|
||
| → 객체지향원칙의 개방폐쇄원칙 위반! | ||
|
|
||
| 아래처럼 새로운 무기가 추가되더라도 이 코드는 수정할 필요 없어야한다. | ||
|
|
||
| ```tsx | ||
| void attack() { | ||
| currentWeapon->doAttack(); | ||
| } | ||
| ``` | ||
|
|
||
|  | ||
|
|
||
| 인증 로직 등을 구현할 때 독립된 전략으로 캡슐화해서 필요에 따라 교체하여 사용할 수 있어야한다. | ||
|
|
||
| 만약에 전략패턴을 사용하지 않는다면 위의 예시와 마찬가지로 passport.authenticate 함수를 다음과 같이 만들어야한다. | ||
|
|
||
| ```tsx | ||
| passport.authenticate = (type) => { | ||
| if (type === 'local') { | ||
| // 로컬 로그인 로직... | ||
| } else if (type === 'google') { | ||
| // 구글 로그인 로직... | ||
| } else if (type === 'facebook') { | ||
| // 페이스북 로그인 로직... | ||
| } | ||
| // 새로운 인증 방식이 나올 때마다 이 코드를 계속 고쳐야 함 | ||
| // (OCP 위반) | ||
| }; | ||
|
|
||
| ``` | ||
|
|
||
| 아래 방식은 전략 파턴을 사용하여 개방-패쇄 원칙을 준수한 코드이다. | ||
|
|
||
| ```tsx | ||
| const passport = require('passport'); | ||
| const LocalStrategy = require('passport-local').Strategy; | ||
|
|
||
| // '로컬 로그인'이라는 전략을 정의 | ||
| // 여기서 use는 전략 등록 함수이다. | ||
| passport.use(new LocalStrategy( | ||
| (username, password, done) => { | ||
| // 실제 DB에서 사용자 확인 로직 (전략 내용) | ||
| User.findOne({ username }, (err, user) => { | ||
| if (err) return done(err); | ||
| if (!user) return done(null, false); | ||
| return done(null, user); | ||
| }); | ||
| } | ||
| )); | ||
|
|
||
| // 라우트에서는 어떤 전략을 쓸지만 결정 | ||
| app.post('/login', passport.authenticate('local'), (req, res) => { | ||
| res.send('로그인 성공!'); | ||
| }); | ||
|
|
||
| ``` | ||
|
|
||
| 전략 패턴을 사용해서 Nestjs의 Strategy Passport 전략을 구현해보자. | ||
|
|
||
| 먼저, 모든 전략이 반드시 가지고 있어야하는 메서드를 정의한다. 위의 예시에서는 attack이다. | ||
|
|
||
| ```tsx | ||
| class AuthStrategy { | ||
| validate(data) { | ||
| throw new Error("validate 메소드 구현해야함"); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| 이제 구체적인 전략 클래스를 만들어보자. | ||
|
|
||
| ```tsx | ||
| // 1. 로컬 로그인 전략 | ||
| class LocalStrategy extends AuthStrategy { | ||
| validate({ username, password }) { | ||
| console.log(`DB에서 ${username}을 찾고 비밀번호를 검증합니다...`); | ||
| // 성공 시 유저 객체 반환 | ||
| return { id: 1, name: username, method: 'local' }; | ||
| } | ||
| } | ||
|
|
||
| // 2. 구글 로그인 전략 | ||
| class GoogleStrategy extends AuthStrategy { | ||
| validate({ accessToken }) { | ||
| console.log(`구글 서버에 토큰 ${accessToken}을 보내 유저 정보를 가져옵니다...`); | ||
| return { id: 99, name: '구글유저', method: 'google' }; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| 전략을 보관하고 실행하는 메니저 클래스를 만든다. 위의 예시에서는 포켓몬 클래스가 될 것이다. 내가 어떤 인증 방식을 사용할 것인지 등록할 수 있어야하고, 실제 인증을 실행하는 함수도 가지고있어야 한다. 이때 인증은 위에서 만든 AuthStrategy에 위임하는 방식이다. | ||
|
|
||
| ```tsx | ||
| class AuthManager { | ||
| constructor() { | ||
| this.strategies = new Map(); // 전략 보관함 | ||
| } | ||
|
|
||
| // NestJS의 passport.use() 같은 역할 | ||
| use(name, strategy) { | ||
| this.strategies.set(name, strategy); | ||
| } | ||
|
|
||
| // 실제 인증 실행 | ||
| authenticate(name, data) { | ||
| const strategy = this.strategies.get(name); | ||
| if (!strategy) { | ||
| throw new Error(`${name} 전략을 찾을 수 없습니다.`); | ||
| } | ||
| return strategy.validate(data); // 위임(Delegation) | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| 위와 같이 구현했으면 실제 코드에서는 아래처럼 편리하게 사용할 수 있게 된다. | ||
|
|
||
| ```tsx | ||
| const authManager = new AuthManager(); | ||
|
|
||
| // 전략 | ||
| authManager.use('local', new LocalStrategy()); | ||
| authManager.use('google', new GoogleStrategy()); | ||
|
|
||
| // 실행 | ||
| const loginType = 'local'; // 'google'로 바꾸면 바로 동작이 변함 | ||
| const userData = authManager.authenticate(loginType, { username: 'gemini', password: '123' }); | ||
|
|
||
| console.log('로그인 성공 결과:', userData); | ||
| ``` | ||
|
|
||
| ### 싱글톤 패턴 | ||
|
|
||
| 노드는 모듈을 처음 불러올 때 그 결과(인스턴스)를 캐싱한다. | ||
|
|
||
| 따라서 이 인스턴스가 메모리에 저장되어있는데, 웬만하면 다른 파일에서도 메모리에 저장된 기존 객체를 사용하는 것이 좋을것이다. | ||
|
|
||
| 단순하게 아래처럼 사용하면 두 객체는 메모리에 매번 새로운 객체가 생성된다. | ||
|
|
||
| ```tsx | ||
| export class UserService { ... } | ||
|
|
||
| // A.js와 B.js에서 각각 | ||
| const service = new UserService(); // A와 B는 서로 다른 객체를 가짐 | ||
| ``` | ||
|
|
||
| 싱글톤으로 만들기 위해서는 인스턴스를 export해야한다. | ||
|
|
||
| ```tsx | ||
| // UserService.js | ||
| class UserService { ... } | ||
|
|
||
| export const userService = new UserService(); // 딱 한 번만 실행됨 | ||
|
|
||
| // A.js와 B.js에서 각각 | ||
| import { userService } from './UserService.js'; // A와 B는 동일한 객체를 공유 | ||
| ``` | ||
|
|
||
| Express + tsyringe를 사용해서 싱글톤 다루기 | ||
|
|
||
| ```tsx | ||
| import { singleton, container } from "tsyringe"; | ||
|
|
||
| @singleton() // 싱글톤으로 지정 | ||
| class DatabaseService { | ||
| constructor() { console.log("DB 연결됨"); } | ||
| } | ||
|
|
||
| @singleton() | ||
| class UserService { | ||
| constructor(private db: DatabaseService) {} // 의존성 자동 주입 | ||
| } | ||
|
|
||
| // 컨테이너를 통해 인스턴스를 가져옴 | ||
| const service1 = container.resolve(UserService); | ||
| const service2 = container.resolve(UserService); | ||
|
|
||
| console.log(service1 === service2); // true (싱글톤) | ||
| ``` | ||
|
|
Collaborator
There was a problem hiding this comment.
예제 코드까지 가져와서 설명하신거 진짜 너무 좋네요 👍👍👍👍👍
저도 보면서 공부를 더 해봐야겠습니다 LGTM!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
✅ 워크북 체크리스트
✅ 컨벤션 체크리스트
📌 주안점