diff --git a/docs/posts/cs/restful-api.mdx b/docs/posts/cs/restful-api.mdx new file mode 100644 index 0000000..d01b98c --- /dev/null +++ b/docs/posts/cs/restful-api.mdx @@ -0,0 +1,346 @@ +--- +title: 'RESTful API 설계 원칙' +type: 'concept' +language: 'CS' +tags: + - API + - REST + - HTTP + - Web +dateModified: 2025.10.10 +--- + +# RESTful API + +REST(Representational State Transfer)는 웹의 장점을 최대한 활용하는 네트워크 아키텍처 스타일입니다. RESTful API는 REST 원칙을 따르는 API로, 자원(Resource)을 URI로 표현하고 HTTP 메서드를 통해 자원에 대한 행위를 정의합니다. + +## REST의 6가지 제약 조건 + +REST 아키텍처는 다음 6가지 제약 조건을 따릅니다. + +**1. 클라이언트-서버 (Client-Server)** +- 클라이언트와 서버가 독립적으로 분리되어 있어야 합니다. +- 각각 독립적으로 개선될 수 있습니다. + +**2. 무상태성 (Stateless)** +- 각 요청은 독립적이며, 서버는 클라이언트의 상태를 저장하지 않습니다. +- 모든 요청에는 필요한 정보가 포함되어야 합니다. + +**3. 캐시 가능 (Cacheable)** +- 응답은 캐시 가능 여부를 명시해야 합니다. +- 클라이언트는 응답을 재사용하여 성능을 향상시킬 수 있습니다. + +**4. 계층화 시스템 (Layered System)** +- 클라이언트는 직접 서버에 연결되었는지, 중간 서버(프록시, 게이트웨이 등)를 통해 연결되었는지 알 수 없어야 합니다. + +**5. 인터페이스 일관성 (Uniform Interface)** +- 일관된 인터페이스를 통해 시스템 아키텍처를 단순화하고, 상호작용의 가시성을 높입니다. + +**6. 코드 온 디맨드 (Code on Demand) - 선택사항** +- 서버가 클라이언트에게 실행 가능한 코드를 전송하여 기능을 확장할 수 있습니다. +- 이는 선택적 제약 조건입니다. + +## HTTP 메서드와 CRUD 매핑 + +REST API는 HTTP 메서드를 사용하여 자원에 대한 행위를 표현합니다. + +| HTTP 메서드 | CRUD | 역할 | 멱등성 | +|------------|------|------|--------| +| GET | Read | 자원 조회 | O | +| POST | Create | 자원 생성 | X | +| PUT | Update | 자원 전체 수정 | O | +| PATCH | Update | 자원 부분 수정 | X | +| DELETE | Delete | 자원 삭제 | O | + +### 멱등성 (Idempotent) + +같은 요청을 여러 번 수행해도 결과가 동일한 성질을 말합니다. GET, PUT, DELETE는 멱등성을 가지지만, POST와 PATCH는 멱등성을 보장하지 않습니다. + +#### 예시 + +```javascript +// GET - 멱등성 O (여러 번 조회해도 결과 동일) +GET /api/users/123 + +// POST - 멱등성 X (요청마다 새로운 자원 생성) +POST /api/users +{ + "name": "홍길동", + "email": "hong@example.com" +} + +// PUT - 멱등성 O (여러 번 수정해도 최종 결과 동일) +PUT /api/users/123 +{ + "name": "홍길동", + "email": "hong@example.com" +} + +// DELETE - 멱등성 O (이미 삭제된 자원을 다시 삭제해도 결과 동일) +DELETE /api/users/123 +``` + +## URI 설계 규칙 + +RESTful API의 URI는 자원을 명확하게 표현해야 합니다. 다음은 좋은 URI를 설계하기 위한 핵심 원칙들입니다. + +**1. 명사를 사용하고 복수형으로 표현** +- 자원을 나타낼 때는 동사가 아닌 명사를 사용하고, 복수형으로 표현합니다. + +**2. 계층 관계는 슬래시(/)로 표현** +- 자원 간의 계층 관계를 명확하게 나타냅니다. + +**3. URI 마지막에 슬래시(/) 포함하지 않기** +- 불필요한 혼란을 방지합니다. + +**4. 하이픈(-)은 사용 가능, 언더스코어(_)는 지양** +- 가독성을 위해 하이픈을 사용합니다. + +**5. 소문자 사용** +- URI는 대소문자를 구분하므로 일관성을 위해 소문자를 사용합니다. + +**6. 파일 확장자는 URI에 포함하지 않기** +- Content-Type 헤더로 미디어 타입을 지정합니다. + +```javascript +// ✅ 좋은 예 +GET /api/users +GET /api/users/123 +GET /api/users/123/orders +GET /api/users/123/orders/456 +GET /api/order-items +GET /api/users/123/profile-image + +// ❌ 나쁜 예 +GET /api/getUsers +GET /api/user +GET /api/users/123/ +GET /api/Users/123/ProfileImage +GET /api/order_items +GET /api/users/123/profile-image.png +``` + +## HTTP 상태 코드 활용 + +적절한 HTTP 상태 코드를 사용하여 응답 결과를 명확히 전달합니다. + +| 상태 코드 | 의미 | 설명 | 사용 시점 | +|----------|------|------|-----------| +| **2xx - 성공** |||| +| 200 OK | 요청 성공 | 요청이 성공적으로 처리됨 | GET, PUT, PATCH | +| 201 Created | 자원 생성 성공 | 새로운 자원이 생성됨 | POST | +| 204 No Content | 요청 성공, 응답 본문 없음 | 요청은 성공했지만 반환할 내용이 없음 | DELETE | +| **3xx - 리다이렉션** |||| +| 301 Moved Permanently | 영구 이동 | 자원의 URI가 영구적으로 변경됨 | URI 변경 | +| 302 Found | 임시 이동 | 자원의 URI가 일시적으로 변경됨 | 임시 URI 변경 | +| **4xx - 클라이언트 오류** |||| +| 400 Bad Request | 잘못된 요청 | 요청 형식이 잘못되었거나 유효하지 않음 | 잘못된 파라미터 | +| 401 Unauthorized | 인증 필요 | 인증이 필요한 요청 | 로그인 필요 | +| 403 Forbidden | 권한 없음 | 인증은 되었지만 권한이 없음 | 권한 부족 | +| 404 Not Found | 자원을 찾을 수 없음 | 요청한 자원이 존재하지 않음 | 존재하지 않는 URI | +| 409 Conflict | 충돌 | 요청이 현재 서버 상태와 충돌 | 중복된 자원 생성 | +| **5xx - 서버 오류** |||| +| 500 Internal Server Error | 서버 내부 오류 | 서버에서 예상치 못한 오류 발생 | 서버 에러 | +| 502 Bad Gateway | 게이트웨이 오류 | 게이트웨이나 프록시 서버에서 오류 발생 | 프록시 에러 | +| 503 Service Unavailable | 서비스 이용 불가 | 서버가 일시적으로 요청을 처리할 수 없음 | 서버 과부하 | + +### 예시 + +```javascript +// 사용자 조회 성공 +GET /api/users/123 +Response: 200 OK +{ + "id": 123, + "name": "홍길동", + "email": "hong@example.com" +} + +// 사용자 생성 성공 +POST /api/users +Response: 201 Created +Location: /api/users/124 +{ + "id": 124, + "name": "김철수", + "email": "kim@example.com" +} + +// 사용자 삭제 성공 +DELETE /api/users/123 +Response: 204 No Content + +// 존재하지 않는 사용자 조회 +GET /api/users/999 +Response: 404 Not Found +{ + "error": "User not found" +} + +// 잘못된 요청 +POST /api/users +{ + "name": "" // 빈 이름 +} +Response: 400 Bad Request +{ + "error": "Name is required" +} +``` + +## RESTful API 설계 예시 + +### 사용자 관리 API + +```javascript +// 사용자 목록 조회 +GET /api/users + +// 특정 사용자 조회 +GET /api/users/123 + +// 사용자 생성 +POST /api/users +{ + "name": "홍길동", + "email": "hong@example.com" +} + +// 사용자 전체 수정 +PUT /api/users/123 +{ + "name": "홍길동", + "email": "hong@example.com", + "phone": "010-1234-5678" +} + +// 사용자 부분 수정 (이메일만 변경) +PATCH /api/users/123 +{ + "email": "newemail@example.com" +} + +// 사용자 삭제 +DELETE /api/users/123 +``` + +### 사용자의 주문 목록 조회 + +```javascript +// 특정 사용자의 모든 주문 조회 +GET /api/users/123/orders + +// 특정 사용자의 특정 주문 조회 +GET /api/users/123/orders/456 + +// 특정 사용자의 새 주문 생성 +POST /api/users/123/orders +{ + "productId": 789, + "quantity": 2 +} +``` + +### 검색 및 필터링 + +```javascript +// 쿼리 파라미터를 사용한 검색 +GET /api/users?name=홍길동 + +// 페이지네이션 +GET /api/users?page=1&limit=20 + +// 정렬 +GET /api/users?sort=createdAt&order=desc + +// 복합 필터링 +GET /api/users?status=active&role=admin&page=1&limit=10 +``` + +## REST vs GraphQL + +REST API의 대안으로 GraphQL이 있습니다. 각각의 특징을 비교하면 다음과 같습니다. + +| 특징 | REST | GraphQL | +|------|------|---------| +| 데이터 요청 | 여러 엔드포인트 | 단일 엔드포인트 | +| 응답 형태 | 서버가 정의한 고정 구조 | 클라이언트가 필요한 데이터만 요청 | +| Over-fetching | 발생 가능 (불필요한 데이터 포함) | 없음 (필요한 데이터만) | +| Under-fetching | 발생 가능 (추가 요청 필요) | 없음 (한 번에 모든 데이터) | +| 캐싱 | HTTP 캐싱 활용 용이 | 추가 구현 필요 | +| 학습 곡선 | 낮음 | 높음 | + +#### 예시 + +```javascript +// REST API - 사용자 정보와 게시글을 가져오려면 2번의 요청 필요 +GET /api/users/123 +GET /api/users/123/posts + +// GraphQL - 한 번의 요청으로 필요한 데이터만 가져오기 +POST /graphql +{ + query { + user(id: 123) { + name + email + posts { + title + createdAt + } + } + } +} +``` + +## 실무 적용 팁 + +**1. API 버저닝** + +API가 변경될 때 기존 클라이언트에 영향을 주지 않도록 버전 관리를 합니다. + +```javascript +// URI에 버전 포함 +GET /api/v1/users +GET /api/v2/users + +// 헤더에 버전 포함 +GET /api/users +Accept: application/vnd.company.v1+json +``` + +**2. 에러 응답 일관성 유지** + +에러 응답 형식을 일관되게 유지하여 클라이언트에서 처리하기 쉽게 합니다. + +```javascript +{ + "error": { + "code": "USER_NOT_FOUND", + "message": "사용자를 찾을 수 없습니다.", + "details": { + "userId": 123 + } + } +} +``` + +**3. HATEOAS (Hypermedia As The Engine Of Application State)** + +응답에 다음 가능한 행동을 링크로 포함하여 클라이언트가 API를 탐색할 수 있게 합니다. + +```javascript +{ + "id": 123, + "name": "홍길동", + "email": "hong@example.com", + "_links": { + "self": { "href": "/api/users/123" }, + "orders": { "href": "/api/users/123/orders" }, + "edit": { "href": "/api/users/123" }, + "delete": { "href": "/api/users/123" } + } +} +``` + +RESTful API는 웹의 기본 원칙을 따르면서도 확장 가능하고 유지보수가 쉬운 API를 설계할 수 있게 해줍니다. 위의 원칙들을 잘 이해하고 적용하면 클라이언트와 서버 간의 효율적인 통신을 구현할 수 있습니다. diff --git a/docs/posts/cs/single-point-of-failure.mdx b/docs/posts/cs/single-point-of-failure.mdx new file mode 100644 index 0000000..27457d1 --- /dev/null +++ b/docs/posts/cs/single-point-of-failure.mdx @@ -0,0 +1,75 @@ +--- +title: '단일 장애 지점(Single Point Of Failure)' +type: 'concept' +language: 'CS' +tags: + - SPOF +dateModified: 2025.09.29 +--- + +# 단일 장애 지점(Single Point Of Failure) + +SPOF 문제는 개발 인프라를 구성할 때 빼 놓을 수 없는 고려점 입니다. + +**SPOF(Single Point of Failure)** 는 단일 구성 요소에 장애가 발생했을 때, 전체 시스템이 영향을 받아 서비스를 이용할 수 없게 되는 상황을 의미 합니다. +예를 들어, 클라이언트가 요청을 보내는 서버가 하나뿐이라면, 해당 서버에 장애가 발생할 경우 서비스를 이용할 수 없게 됩니다. + +각 계층별로 SPOF는 반드시 고려해봐야 할 문제 입니다. +예를 들어, 클라이언트가 서버에 요청을 진행하면 L4 로드밸런서가 요청을 받아 각 Proxy Server에 데이터를 넘기며, Proxy 서버는 요청을 내부의 WAS로 전달 합니다. WAS에서는 Database에 있는 값을 확인해 생성된 데이터를 Client에게 반환 합니다. + +```jsx +client -> L4 Load Balancer -> Proxy Server -> WAS -> Database +``` +--- + +### L4 Load Balancer + +L4 로드밸런서는 TCP/UDP 레벨에서 요청을 분산 처리하는 역할을 합니다. + +해당 계층에서 SPOF를 방지하기 위한 방법은 다음과 같습니다. + +- **이중화 구성 (Active-Active 또는 Active-Passive)** +- **VRRP(Virtual Router Redundancy Protocol)** 를 통한 고가용성 구성 +- **클라우드 환경에서는 관리형 L4 (예: AWS NLB, Azure Load Balancer)** 를 활용해 내결함성 확보 + +**SPOF 예방 방안** + +- L4 LB를 두 대 이상 배치 +- 헬스 체크 기반의 자동 장애 전환(failover) 구성 + +## Proxy Server + +Proxy 서버는 클라이언트 요청을 받아 내부 WAS로 전달하거나 캐싱, 인증, 보안 처리 등의 역할을 합니다. + +프록시가 하나일 경우 SPOF가 발생하므로 **수평 확장 및 L4 앞단 분산**이 필수입니다. + +**SPOF 예방 방안** + +- 여러 대의 프록시 서버를 L4 로드밸런서를 통해 분산 +- Stateless 설계로 프록시 간 장애 영향을 최소화 +- Proxy 레벨에서의 Failover 및 헬스 체크 기능 사용 + +## WAS + +WAS는 실제 비즈니스 로직이 실행되는 계층이다. 하나의 WAS 인스턴스에 모든 처리를 맡길 경우, 서비스 전체 장애가 발생할 수 있습니다. + +**SPOF 예방 방안** + +- WAS 인스턴스를 **다중 구성(Auto Scaling 포함)** +- Proxy 또는 L7 Load Balancer에서 **로드밸런싱** +- **세션 클러스터링 또는 세션 스토리지 분리(Redis 등)** 를 통해 무상태 아키텍처 구현 + +## Database + +데이터베이스는 서비스의 핵심 데이터를 저장하므로, 장애 시 영향도가 가장 큽니다. + +DB는 전통적으로 SPOF가 발생하기 쉬운 계층이므로, 고가용성 구성이 필수 입니다. + +**SPOF 예방 방안** + +- **Master-Slave 또는 Master-Master 복제 구성** +- **리더-팔로워 구조 + 자동 Failover (예: RDS Multi-AZ, MySQL with MHA)** +- **샤딩 또는 분산 DB** 도입 고려 +- 정기 백업 및 장애 복구 절차 수립 + +SPOF는 모든 인프라 구성에서 반드시 고려해야 할 요소이며, 계층별로 고가용성을 확보하는 구조 설계가 안정적인 시스템 운영의 핵심입니다. diff --git a/docs/posts/database/transaction-Isolation-level.mdx b/docs/posts/database/transaction-Isolation-level.mdx new file mode 100644 index 0000000..f01c20a --- /dev/null +++ b/docs/posts/database/transaction-Isolation-level.mdx @@ -0,0 +1,87 @@ +--- +title: 트랜잭션 격리 수준 +type: 'concept' +language: Database +tags: + - Database + - Transaction +dateModified: 2025.09.28 +--- + +import { ImageContainer } from '/src/components/ui/ImageContainer.tsx'; + +# 트랜잭션 격리 수준(Transaction Isolation Level)이란? + +- 동시에 여러 트랜잭션이 처리될 때, 트랜잭션끼리 얼마나 서로 고립(격리)되어 있는지를 나타내는 것입니다. +- 즉, 특정 트랜잭션이 다른 트랜잭션에서 변경한 데이터를 볼 수 있는지를 결정합니다. + +격리 수준은 크게 4가지로 나뉩니다. + +- Read Uncommitted (트렌젝션 레벨 0) +- Read Committed (트렌젝션 레벨 1) +- Repeatable Read (트렌젝션 레벨 2) +- Serializable (트렌젝션 레벨 3) + +## Read Uncommitted + +- 다른 트랜잭션에서 커밋되지 않은 데이터를 읽을 수 있습니다. +- 데이터 정합성에 심각한 문제가 발생할 수 있으므로, 일반적으로 사용을 권장하지 않습니다. +- Dirty Read, Non-Repeatable Read, Phantom Read가 발생할 수 있습니다. + +## Read Committed + +- 다른 트랜잭션에서 커밋이 완료된 데이터만 읽을 수 있습니다. +- 한 트랜잭션이 수행되는 동안 다른 트랜잭션은 대기합니다. +- 대부분의 RDB에서 Default로 사용됩니다. +- Dirty Read는 방지할 수 있으나, Non-Repeatable Read와 Phantom Read는 발생할 수 있습니다. + +## Repeatable Read + +- 트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 읽을 수 있습니다. +- MySQL에서 Default로 사용됩니다. +- 새로운 레코드가 추가되는 경우에는 동일한 조회 결과를 보장하지 않으므로 Phantom Reads가 발생할 수 있습니다. + +## Serializable + +- 하나의 트랜잭션에서 사용하는 레코드는 다른 트랜잭션에서 절대 접근할 수 없습니다. +- 가장 단순하면서도 엄격한 격리 수준으로, 데이터 정합성이 보장되지만 DB Lock이 많이 걸려 동시성이 가장 떨어집니다. + +# 트랜잭션의 이상 현상 (Anomaly) + +## Dirty Read + +- 한 트랜잭션에 의해 변경되었지만 아직 커밋되지 않은 데이터를 다른 트랜잭션에서 읽을 때 발생하는 현상입니다. +- 이때 읽은 값이 롤백되면 잘못된 데이터가 반영되어 데이터 정합성 문제가 발생할 수 있습니다. + + + +## Non-Repeatable Read + +- 한 트랜잭션 내에서 동일한 SELECT 쿼리를 두 번 실행했을 때, 그 결과가 서로 다르게 나타나는 현상입니다. + + + +## Phantom Reads + +- 한 트랜잭션 안에서 같은 조건으로 두 번 읽을 때, 중간에 다른 트랜잭션이 데이터를 추가/삭제해서 결과가 달라지는 현상입니다. + + + +# 정리 + +| | 설명 | 해결 가능한 격리 수준 | +| ------------------- | ------------------------------------------------------------------------------------- | --------------------- | +| Dirty Read | 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 문제 | READ COMMITTED 이상 | +| Non-Repeatable Read | 같은 데이터를 두 번 읽었을 때 값이 달라지는 문제 | REPEATABLE READ 이상 | +| Phantom Read | 같은 조건으로 조회했는데, 다른 트랜잭션이 데이터를 추가/삭제하여 결과가 달라지는 문제 | SERIALIZABLE | + +- 격리 수준이 낮을수록 동시성(성능)은 높아지지만, 데이터 정합성이 떨어질 위험이 커집니다. +- 격리 수준이 높을수록 데이터 정합성은 보장되지만, 성능이 저하될 수 있습니다. diff --git a/docs/posts/database/transaction.mdx b/docs/posts/database/transaction.mdx new file mode 100644 index 0000000..7e4d8a3 --- /dev/null +++ b/docs/posts/database/transaction.mdx @@ -0,0 +1,86 @@ +--- +title: '트랜잭션(Transaction)' +type: 'concept' +language: 'SQL' +tags: + - Database + - Transaction +dateModified: 2025.09.21 +--- + +# 트랜잭션(Transaction)이란? + +트랜잭션은 데이터베이스의 논리적 작업 단위입니다. +데이터베이스의 상태를 바꾸기 위해 실행되는 일련의 연산들을 의미하며, 하나의 작업이 성공적으로 수행되면 데이터베이스는 영구적으로 변경됩니다. 반대로 실패하면 모든 변경 사항은 취소됩니다. + +--- + +## 작업 단위란? + +DB에서 작업 단위(Unit of Work)는 하나의 논리적인 작업을 수행하는 최소 단위입니다. 즉, 여러 개의 SQL 명령문을 하나로 묶어 "이 작업은 반드시 함께 처리되어야 한다"라는 조건을 만족하는 연산 집합을 의미합니다. + +### 예제 1: 은행 계좌 이체 +- A 계좌에서 50,000원 출금 +- B 계좌에 50,000원 입금 + +위 두 작업은 반드시 함께 실행되어야 합니다. 만약 중간에 오류가 발생해 A 계좌에서만 출금되고 B 계좌에 입금되지 않았다면 전체 작업은 취소(ROLLBACK)되어야 합니다. + +### 예제 2: 쇼핑몰 주문 처리 +- 상품 재고 감소 +- 주문 내역 저장 +- 결제 내역 저장 + +세 가지 작업이 모두 성공해야만 주문이 정상적으로 완료됩니다. 하나라도 실패하면 전체를 취소(ROLLBACK)하여 처음 상태로 되돌립니다. + +## 트랜잭션과의 관계 + +작업 단위는 트랜잭션이 보장하는 논리적인 작업 덩어리입니다. 트랜잭션은 작업 단위를 묶어서 한 번에 처리하거나 오류 발생 시 전부 취소합니다. +따라서 트랜잭션은 **작업 단위의 온전한 실행을 보장**하는 역할을 합니다. + +# 트랜잭션의 특성 (ACID) + +트랜잭션이 올바르게 수행되기 위해서는 **ACID 원칙**을 준수해야 합니다. + +## 1. Atomicity (원자성) + +트랜잭션의 연산은 모두 수행되거나, 하나도 수행되지 않아야 합니다. 예를 들어 A 계좌에서 돈이 빠져나갔는데 B 계좌에 입금되지 않는 상황은 허용되지 않습니다. + +원자성은 롤백(ROLLBACK)과 세이브포인트(SAVEPOINT)를 통해 보장됩니다. + +```sql +commit; + +update dept set dname = 'QC2' where deptno=70; +savepoint SP1; + +delete from dept where deptno = 70; +savepoint SP2; + +insert into dept values (50, '부서명', '지역명'); + +rollback; -- 마지막 commit 시점으로 전체 취소 +rollback to savepoint SP1; -- SP1 이후 작업만 취소 +``` + +## 2. Consistency (일관성) + +트랜잭션 전후로 데이터베이스의 무결성이 유지되어야 합니다. +예를 들어 은행 계좌의 전체 잔액 합계는 변하지 않아야 합니다. + +일관성은 주로 트리거(Trigger) 등을 통해 자동으로 보장합니다. + +## 3. Isolation (고립성) + +여러 트랜잭션이 동시에 실행되더라도 서로의 작업에 영향을 주어서는 안 됩니다. +한 트랜잭션의 결과는 해당 트랜잭션이 완료될 때까지 다른 트랜잭션에서 참조할 수 없습니다. + +## 4. Durability (지속성) + +트랜잭션이 완료되면 시스템 장애가 발생하더라도 결과는 유지되어야 합니다. +이를 위해 Write Ahead Logging(WAL) 같은 기술이 사용됩니다. + +# 추가 단어 개념 정리 +- rollback : 실행된 트랜잭션을 취소하고 이전 상태로 되돌리는 것을 의미합니다. +- savepoint : 트랜잭션 중간에 되돌아갈 지점을 지정하는 것을 의미합니다. ROLLBACK TO 명령으로 해당 시점 이후 작업만 취소가 가능합니다. +- trigger : 특정 이벤트(insert, update, delete 등)가 발생했을 때 자동으로 실행되는 SQL 코드입니다. 일관성 유지에 활용됩니다. +- WAL (Write Ahead Logging) : 변경 사항을 실제 DB에 적용하기 전에 로그에 먼저 기록하는 방식을 의미합니다. 장애가 발생해도 로그를 이용해 데이터를 복구할 수 있습니다. \ No newline at end of file diff --git a/docs/posts/fe-interview/storage-cookie-session.mdx b/docs/posts/fe-interview/storage-cookie-session.mdx new file mode 100644 index 0000000..ecbb1d0 --- /dev/null +++ b/docs/posts/fe-interview/storage-cookie-session.mdx @@ -0,0 +1,73 @@ +--- +title: 쿠키, 세션, 웹스토리지의 차이점 +type: 'interview' +language: 'FE-Interview' +tags: + - Web Storage + - Cookie + - Session +dateModified: 2025.10.06 +--- + +## 답변 + +쿠키, 세션, 웹스토리지는 모두 사용자 상태를 저장하는 방법이지만, 저장 위치와 유지 방식에서 차이점이 있습니다. + +- **쿠키(Cookie)** 는 클라이언트에 저장되지만, 서버에 자동 전송되는 데이터로, 주로 로그인 유지나 트래킹에 사용됩니다. +- **세션(Session)** 은 서버에 저장되는 사용자 상태 정보로, 보안성이 높고 인증 관리에 주로 사용됩니다. +- **웹 스토리지(Web Storage)** 는 브라우저에만 저장되는 데이터로, 서버로 전송되지 않아 성능이 좋고 상대적으로 용량이 크지만, 보안적으로 민감한 정보를 저장하는 데에는 부적합합니다. + +민감한 인증 정보는 가능한 HttpOnly, Secure, SameSite가 설정된 쿠키(또는 서버 세션)로 관리하고, localStorage에는 비민감한 UX 관련 데이터만 저장하는 것이 안전합니다. + +
+ +### Q. 세션 데이터를 구분하는 세션 ID는 어디에 저장되나요? + +일반적으로 서버는 로그인 시 세션 데이터를 생성하고, 브라우저에 세션 ID를 쿠키로 저장(Set-Cookie 헤더)합니다. 이후 요청 시 이 세션 ID가 함께 전송되어 서버에서 사용자를 식별합니다. + +
+ +### Q. 쿠키의 보안상 위험성에 대해 설명해주세요. + +쿠키는 자동 전송되는 특성 때문에 공격자가 사용자 권한으로 요청을 보낼 수 있어서 \*CSRF 방어가 필수적이며, +JavaScript로 쿠키를 읽어 탈취할 수 있어 \*XSS 위험이 있습니다. + +이를 방지하기 위해서는 다음과 같은 옵션을 설정할 수 있습니다. + +- **HttpOnly** : HttpOnly 쿠키는 클라이언트 JS에서 접근 불가능하므로 XSS 기반 쿠키 탈취를 방지하는 데 유효합니다. (단, 서버에서만 설정 가능합니다.) +- **Secure** : HTTPS 통신에서만 전송 +- **SameSite** : 쿠키가 크로스 사이트 컨텍스트에서 전송되는 방식을 제어하여 \*CSRF 공격을 방지합니다. (Strict, Lax, None 세 가지 모드가 있으며, None을 사용하려면 Secure 옵션이 반드시 필요합니다.) + +
+ +### Q. localStorage와 sessionStorage의 차이를 설명해주세요. + +모두 브라우저에 저장되는 key-value 형태의 데이터이지만, +localStorage는 브라우저를 꺼도 유지되지만, sessionStorage는 탭이 닫히면 삭제된다는 점이 차이입니다. + +
+ +### Q. JWT와 위 스토리지들의 관련성에 대해 설명해주세요. + +JWT(Json Web Token)는 서버에 세션을 저장하지 않는 stateless 인증 방식으로, 확장성이 뛰어나지만 보안 관리(만료, 탈취, 재발급)에 특히 주의해야 합니다. + +저장 시에 HttpOnly Secure Cookie에 넣는 것이 보안상 안전한 방법입니다. + +
+ +### 요약 + +| 구분 | 쿠키 (Cookie) | 세션 (Session) | 웹 스토리지 (Web Storage) | +| --------- | --------------------------------- | ----------------------- | ------------------------------------------------------ | +| 저장 위치 | 클라이언트(브라우저) | 서버 | 클라이언트(브라우저) | +| 만료 시점 | 설정 가능 (기본 브라우저 종료 시) | 브라우저 종료 시 | sessionStorage: 종료 시 / localStorage: 명시적 삭제 시 | +| 용량 제한 | 약 4KB | 서버 메모리 | 약 5MB (브라우저별 차이 있음) | +| 보안 | 요청 시 자동 전송 → 탈취 위험 | 서버 관리로 비교적 안전 | JS 접근 가능 → XSS 취약 가능 | +| 사용 예시 | 로그인 유지, 트래킹 | 로그인 인증 상태 관리 | 사용자 설정, 임시 데이터 저장 | + +
+ +### 용어 + +- **XSS (Cross site Scripting)** : 교차 사이트 스크립팅은 웹사이트에 악성 클라이언트 코드를 삽입하여 민감한 정보를 탈취하거나, 사용자 행위를 대행하거나 웹 사이트를 변조하는 사이버 공격 기법입니다. +- **CSRF(Cross site request forgery)** : 교차 사이트 간 요청 위조는 신뢰할 수 있는 사용자를 사칭하여 악성 요청이 웹 사이트에 전송되도록 유도하는 사이버 공격 기법입니다. diff --git a/docs/posts/network/DNS.mdx b/docs/posts/network/DNS.mdx new file mode 100644 index 0000000..fa85813 --- /dev/null +++ b/docs/posts/network/DNS.mdx @@ -0,0 +1,94 @@ +--- +title: 'DNS(Domain Name System)' +type: 'concept' +language: 'network' +tags: + - Network + - IP + - Protocol + - Domain +dateModified: 2025.10.12 +--- + +import { ImageContainer } from '/src/components/ui/ImageContainer.tsx'; + +# 1. DNS란? + +DNS(Domain Name System)이란 사용자가 웹브라우저에 사람이 읽을 수 있는 도메인 네임을 컴퓨터가 이해할 수 있는 ip주소로 변환하는 분산 데이터베이스 시스템을 의미합니다. + +- ```naver.com```이라는 도메인 명을 입력하면 DNS를 통해 해당 도메인 명과 매핑되어있는 IP 주소로 변환해 브라우저가 인터넷 자원을 로드할 수 있도록 도와줍니다. + - 도메인 이름 : 체계적인 문자로 표현한 호스트 이름 + - www.naver.com 의 주소 중 naver.com 부분을 의미합니다. + - www는 호스트 네임(host name)이라고 불립니다. + +- DNS는 여러 개의 DNS 서버(혹은 네임 서버)들로 구성된 분산 시스템입니다. + - OSI 7 계층 중, 애플리케이션 계층에서 동작하는 프로토콜 + - 기본적으로 UDP/53을 기반으로 사용하지만, 특수한 상황에서는 TCP/53도 사용 + +## 도메인 네임 구조 + + + +# 2. DNS 동작 방식 + + + +1. 사용자가 브라우저에 www.naver.com 과 같은 도메인을 입력한 뒤, 엔터를 누릅니다. +2. 현재 컴퓨터에 해당 도메인 명의 IP가 캐싱되어 있거나 C:\Windows\System32\drivers\etc\에 있는 host라는 파일에 있는지 확인하고 없으면 이 URL은 ISP(Internet Service Provider)가 관리하는 DNS resolver에 전송됩니다. +3. DNS resolver는 캐싱 여부를 확인하고 없으면 root 서버에 도메인명을 보내 .com 도메인의 TLD(최상위 도메인) 서버의 IP주소를 알아냅니다. +4. TLD 서버에 다시 URL을 요청하고 모든 도메인 네임에 대한 IP주소를 가지고 있는 Authrotitative Name Server의 IP를 반환합니다. +5. Authrotitative Name Server을 통해 질의하여 해당 도메인과 매핑되는 DNS 레코드의 IP 주소 정보를 찾아서 반환합니다. +6. 해당 도메인과 일치하는 IP주소를 응답 받습니다. +7. DNS resolver는 이 IP주소를 클라이언트에게 다시 전송합니다. + +## DNS 질의 방식 + +1. 재귀적 질의(Recursive Query) +- 클라이언트(사용자)와 재귀적 DNS 서버 사이에서 발생 +- 클라언트 : 재귀적 DNS 서버에게 "내가 원하는 도메인의 IP 주소를 찾아줘"라고 요청 +- 재귀적 DNS 서버(DNS Resolver) : 클라이언트의 요청에 대한 최종적인 답변(IP 주소)을 찾아 클라이언트에게 전달 + +클라이언트 입장에서는 단 한 번의 요청으로 모든 과정을 위임하는 셈입니다. + +2. 반복적 질의(Iterative Query) +- 재귀적 DNS 서버가 다른 DNS 서버들(루트, TLD, 권한 있는 DNS 서버)에게 정보를 요청하는 과정 +- 재귀적 DNS 서버는 IP 주소를 찾기 위해 다음과 같은 과정 반복 (2번 DNS 동작 방식의 일부) + 1. 재귀적 DNS 서버 -> 루트 DNS 서버 (질의) : "내가 찾는 도메인의 정보를 어디서 얻을 수 있니?" + 2. 루트 DNS 서버 -> 재귀적 DNS 서버 (응답) : 루트 DNS 서버는 그 도메인의 TLD(최상위 도메인)를 담당하는 TLD DNS 서버의 주소를 알려줍니다. + 3. 재귀적 DNS 서버 -> TLD DNS 서버 (질의) : "내가 찾는 도메인의 정보를 어디서 얻을 수 있니?" + 4. TLD DNS 서버 -> 재귀적 DNS 서버 (응답) : TLD DNS 서버는 해당 도메인의 정보를 최종적으로 가지고 있는 권한 있는(Authoritative) DNS 서버의 주소를 알려줍니다. + 5. 재귀적 DNS 서버 -> 권한 있는 DNS 서버 (질의) : "내가 찾는 도메인의 정보를 어디서 얻을 수 있니?" + 6. 권한 있는 DNS 서버 -> 재귀적 DNS 서버 (응답) : 최종 IP 주소 + +## DNS 주요 구성 요소 + +각 요소는 DNS가 원활하게 작동하는데 중요한 역할을 합니다. + +1. DNS 리졸버(Resolver) : DNS의 요청을 받아들이고 해당 도메인의 IP 주소를 찾아주는 장치나 소프트웨어입니다. +대부분의 인터넷 서비스 제공자(ISP)에서 이 역할을 수행합니다. +2. 네임 서버(Name Server) : 도메인 이름과 그에 상응하는 IP 주소 정보를 저장하는 서버입니다. 네임 서버는 여러 종류가 있으며, 각각의 역할에 따라 루트 네임 서버, TLD 네임 서버, 권한있는 네임 서버로 구분됩니다. +3. 존 파일(Zone File) : 권한있는 네임 서버에 저장된 파일로, 도메인과 IP 주소의 매핑 정보를 포함하고 있습니다. +4. 레코드(Record) : DNS 레코드는 특정 도메인에 대한 정보를 저장하는 형식입니다. + +# 3. DNS 서버 요청 시 UDP를 사용하는 이유 + +일반적으로 안정적인 데이터 송수신을 위해 TCP를 사용하는 경우가 대부분입니다. 그런데 왜 DNS 서버에 요청을 보낼 때 대부분 UDP를 사용할까요? + +1. TCP에 비해 빠른 전송 + - TCP는 안정적인 데이터 전송을 위해 많은 절차 필요하여 전송 속도가 느릴 수 밖에 없습니다. + - 그러면 여기서 DNS 서버 요청 시에도 그런 절차가 필요하지 않을까? 라는 의문이 듭니다. + 아무리 속도가 빠르다고 해도 DNS 서버에 요청을 할 때 안정적으로 전송되지 않으면 문제가 생길 수 있기 때문입니다. + 게다가 UDP는 TCP와 달리 검출 프로세스가 없기 때문에 다른 사람이 악의적으로 패킷을 가로채서 내용을 변경하더라도 막을 방법이 없습니다. 그래도 왜 UDP를 더 많이 사용할까요? +2. 요청 데이터의 크기가 작아 전송 계층에서 데이터를 쪼갤 필요 x + - 전송 계층에서 데이터그램 하나만으로 충분히 데이터를 담을 수 있으므로, TCP의 이점 중 하나인 순서 보장이 필요 없습니다. +3. TLS 전송 계층에서 암호화 프로토콜이 자주 쓰이기 때문에 전송 계층에서 데이터의 변조가 발생하더라도 이를 통해 잡아낼 수 있습니다. + +- 다만, 2번에서 데이터의 크기가 커서 쪼개야 하거나, 상대 측 방화벽에서 UDP 프로토콜을 사용하는 데이터그램을 차단하는 경우 TCP를 사용해야 합니다. + +# 4. DNS 보안 문제 + +DNS는 네트워크 보안에 있어 매우 중요한 역할을 합니다. 하지만, DNS가 악용될 경우 심각한 보안 문제를 초래할 수 있습니다. + +1. DNS 스푸핑(DNS Spoofing) : 공격자가 가짜 DNS 정보를 제공하여 사용자를 의도하지 않은 웹사이트로 유도하는 공격 방식입니다. 이를 통해 악성 웹사이트로 사용자를 유도하거나 개인 정보를 탈취할 수 있습니다. +2. DNS DDoS 공격 : 분산 서비스 거부(DDoS) 공격은 다수의 장치로부터 DNS 서버에 과도한 요청을 보내 서버를 마비시키는 방식입니다. DNS 서버가 정상적으로 작동하지 않으면, 인터넷 연결에 문제가 생깁니다. +3. DNSSEC : DNS 보안을 강화하기 위해 개발된 기술로, DNS 레코드가 조작되지 않았는지 검증하는 기능을 제공합니다. DNSSEC는 각 레코드에 디지털 서명을 부여해 사용자가 신뢰할 수 있는 DNS 정보를 받을 수 있도록 보장합니다. \ No newline at end of file diff --git a/docs/posts/network/forward-reverse-proxy.mdx b/docs/posts/network/forward-reverse-proxy.mdx new file mode 100644 index 0000000..e5deda6 --- /dev/null +++ b/docs/posts/network/forward-reverse-proxy.mdx @@ -0,0 +1,64 @@ +--- +title: 포워드 프록시와 리버스 프록시 +type: 'concept' +language: Network +tags: + - Network + - Proxy +dateModified: 2025.10.12 +--- + +# 프록시란? + +- 프록시는 '대리'라는 뜻으로, 클라이언트와 서버 사이에 위치하며 통신을 중계하는 역할을 하는 서버입니다. +- 프록시는 네트워크 상의 위치에 따라 포워드 프록시와 리버스 프록시로 나뉩니다. + +# 포워드 프록시 (Forward Proxy) + +`[클라이언트] → [포워드 프록시] → [인터넷] → [웹 서버]` + +- 클라이언트와 인터넷 사이에 위치합니다. +- 클라이언트는 타겟 서버의 주소를 포워드 프록시에 전달하고, 포워드 프록시는 클라이언트를 대신해 해당 서버로 요청을 보내고 응답을 클라이언트에게 반환합니다. +- 일반적으로 프록시라고 하면 포워드 프록시를 의미합니다. + +## 포워드 프록시의 장점 + +### 캐싱 + +- HTML, JavaScript, CSS, 이미지 등의 정적 리소스를 원본 서버로부터 캐싱합니다. +- 이후 동일한 요청이 들어오면 원본 서버에 요청을 전달하지 않고 캐시된 데이터를 반환하여 응답합니다. + +### 액세스 제한 + +- 특정 웹사이트나 불법적인 콘텐츠에 대한 접근을 차단하도록 설정할 수 있습니다. + +### 익명성 + +- 모든 요청이 프록시 서버를 통해 전달되므로 사용자의 실제 IP 주소는 노출되지 않습니다. + +# 리버스 프록시 (Reverse Proxy) + +`[클라이언트] → [인터넷] → [리버스 프록시] → [웹 서버]` + +- 서버와 인터넷 사이에 위치합니다. +- 클라이언트는 리버스 프록시에 요청을 보내고, 리버스 프록시가 이를 원본 서버로 전달하고 받은 응답을 클라이언트에게 반환합니다. + +## 리버스 프록시의 장점 + +### 로드 밸런싱 + +- 여러 서버로 트래픽을 분산시켜 서버 과부하를 방지합니다. +- 특정 서버에 장애가 발생한 경우, 해당 서버로 요청이 전달되지 않도록 설정할 수 있습니다. + +### 서버 보안 + +- 외부에서는 원본 서버가 아닌 프록시 서버의 IP만 알 수 있기 때문에, DDoS 등의 공격으로부터 원본 서버를 보호할 수 있습니다. + +### SSL 암호화 + +- 클라이언트와 서버 간의 SSL/TLS 통신은 암호화·복호화 과정에서 원본 서버에 많은 계산 비용을 발생시킬 수 있습니다. +- SSL/TLS 관련 작업을 프록시에 위임함으로써 원본 서버의 부담을 줄이고 리소스를 절약할 수 있습니다. + +### 캐싱 + +- 포워드 프록시와 동일하게 리버스 프록시도 캐싱 기능을 수행할 수 있습니다. diff --git a/docs/posts/network/http.mdx b/docs/posts/network/http.mdx new file mode 100644 index 0000000..731c883 --- /dev/null +++ b/docs/posts/network/http.mdx @@ -0,0 +1,98 @@ +--- +title: 'HTTP 버전별 특징' +type: 'concept' +language: 'network' +tags: + - HTTP + - Protocol +dateModified: 2025.10.16 +--- + +## HTTP 버전 + +HTTP는 1.0부터 시작해 1.1, 2.0, 3.0에 이르기까지 계속해서 발전해왔습니다. HTTP 버전별 특징과 이전 버전의 단점을 어떻게 극복해 왔는지 살펴보겠습니다. + +### HTTP + +- HTTP (HyperText Transfer Protocol)은 월드 와이드 웹의 기반이 되는 프로토콜 +- HTTP의 변천사 + - HTTP/0.9 : 원-라인 프로토콜 + - HTTP/1.0 : 확장성 만들기 + - HTTP/1.1 : 표준 프로토콜 + - HTTP/2 : 더 나은 성능을 위한 프로토콜 + - HTTP/3 : QUIC를 통한 HTTP + +### WWW (World Wide Web) + +- TCP/IP 프로토콜 기반의 하이퍼텍스트 시스템 +- 4개의 구성요소 + - 하이퍼텍스트 문서를 표현하기 위한 텍스트 형식의 하이퍼텍스트 마크업 언어 (HTML) + - 하이퍼텍스트 문서를 교환하기 위한 '하이퍼텍스트 전송 프로토콜' (HTTP) + - 문서 읽기/쓰기를 위한 클라이언트인 '월드 와이드 웹(WorldWideWeb)'이라고 불리는 첫번째 브라우저 + - 문서에 접근하도록 해주는 서버, 'httpd'의 초기 버전 + + +### HTTP/0.9 + +- 요청은 단일 라인으로 구성되며 리소스에 대한 경로로 가능한 메서드는 `GET`만 존재 +- HTTP 헤더가 없었고, HTML 파일만 전송 가능 + +### HTTP/1.0 + +- 각 요청에 버전 정보가 포함되어 전송(HTTP/1.0이 GET 라인에 붙은 형태 +- 응답 값에 상태 코드 라인 추가 +- 요청 및 응답에 HTTP 개념 추가, 이로 인해 메타데이터 전송 가능 +- Content-Type이 추가되어 HTML 이외의 다른 타입의 문서도 전송 가능 +- 매 요청마다 새로운 TCP 연결을 맺고 끊어야 함 + +``` +GET /mypage.html HTTP/1.0 +User-Agent: NCSA_Mosaic/2.0 (Windows 3.1) + +200 OK +Date: Tue, 15 Nov 1994 08:12:31 GMT +Server: CERN/3.0 libwww/2.17 +Content-Type: text/html + +A page with an image + + +``` + +### HTTP/1.1 + +- 동일 도메인 간 TCP 연결 유지 (지속 연결) `Keep-Alive: timeout=5, max=1000` +- 파이프라이닝 개념 도입 + - 이전에 1개의 TCP 연결에 1:1 요청/응답만 가능했던 개선되어 여러 개의 요청을 보내는 것이 가능 + +- 단점 + - HTTP 문서 내에 포함된 다수의 리소스(image, css, script)를 처리하려면 요청할 리소스의 개수에 비례하여 Latency가 길어짐 + - 클라이언트가 보낸 요청 순서와 동일하게 응답 순서를 맞춰야 함 + - 이전 요청에 대한 응답이 지연될 경우 이후 응답 결과에 대한 처리가 지연(Head of Line Blocking, HOL Blocking) + - RTT(Round Trip Time) 증가 + - 커넥션 한 개에 요청 하나를 처리하는 특성 때문에 매번 요청별로 connection을 만들게 되고 TCP 상에서 동작하는 HTTP의 특성상 3-way-handshake가 반복적으로 발생 + - 불필요한 RTT 증가와 네트워크 지연을 초래하여 성능을 지연시킴 + +### HTTP/2.0 + +- HTTP/1.1을 완전하게 재작성한 것이 아니라 프로토콜의 성능에 초점을 맞춰 수정한 버전. Latency나 네트워크, 서버 리소스 사용량 등과 같은 성능 위주로 개선 + +- Multiplexed Stream + - connection 1개로 동시에 여러 개의 메시지를 주고 받을 수 있으며, 응답은 순서에 상관없이 stream으로 주고 받음 + +- Stream Prioritization + - 리소스 간의 의존관계에 따른 우선순위를 설정하여 리소스 로드 문제를 해결 (ex. 이미지 리소스보다 HTML 리소스 로드가 우선) + +- Header Compression + - HPACK 압축 방식으로 헤더를 압축하여 전송, 중복 헤더 내용의 경우 재전송 하지 않도록 처리 + +- 단일 TCP 연결, 이로 인해 TCP 커넥션 지연 문제를 다소 해결할 수 있었으나, 패킷 손실 감지 및 재전송 시 모든 스트림 차단되는 단점 존재 + +### HTTP/3.0 + +- UDP 기반의 프로토콜인 QUIC(Quick UDP Internet Connection)을 사용 +- 여러 스트림을 실행하고 각 스트림에 대해 독립적으로 패킷 손실 감지 및 재전송하기 때문에 문제가 있는 스트림만 차단 + +----- +#### 참고문서 +[MDN HTTP의 진화](https://developer.mozilla.org/ko/docs/Web/HTTP/Guides/Evolution_of_HTTP) \ No newline at end of file diff --git a/docs/posts/node.js/event-loop.mdx b/docs/posts/node.js/event-loop.mdx new file mode 100644 index 0000000..4bf99bc --- /dev/null +++ b/docs/posts/node.js/event-loop.mdx @@ -0,0 +1,57 @@ +--- +title: 'Node.js의 이벤트 루프 이해하기' +type: 'concept' +language: 'Node.js' +tags: + - Node.js +dateModified: 2025.09.21 +--- + +## 이벤트 루프란? +이벤트 루프(Event Loop)는 Node.js 환경에서 동기 코드와 비동기 코드의 실행을 조정하고 조율하는 디자인 패턴이자 핵심 메커니즘입니다.
+ +## 이벤트 루프가 필요한 이유 +JavaScript는 동기적이며 싱글 스레드라는 특징을 가지고 있습니다.
+이러한 특징은 파일 읽기, 네트워크 요청, CPU 바운드 작업(계산이 오래 걸리는 작업)을 진행할 때 전체 스레드(메인 스레드)가 멈추는 문제가 발생할 수 있습니다.
+이벤트 루프는 이러한 싱글 스레드 환경에서 비동기 작업을 효율적으로 처리할 수 있게 합니다. + +## 이벤트 루프의 구현 +이벤트 루프는 Node.js 런타임을 구성하는 핵심 외부 종속성 중 하나인 `libuv` 라이브러리를 통해 구현됩니다.
+`libuv`는 C로 작성된 크로스 플랫폼 라이브러리로, 비동기 I/O 처리를 지원하며, 이벤트 루프를 구현하여 콜 스택에 어떤 작업을 쌓을지 관장합니다. + +## 이벤트 루프의 동작 순서 +이벤트 루프의 실행은 동기 코드 실행 > 비동기 작업 위임 > 이벤트 루프 단계별 콜백 처리 순서로 이루어집니다. + +### 1. 이벤트 루프 생성 +### 2. 동기 코드 실행 +생성된 이벤트 루프에 진입하기 전에, 동기 코드가 이벤트 루프 바깥에서 처음부터 끝까지 순차적으로 실행됩니다. +### 3. 비동기 작업 위임 +동기 코드를 실행하는 동안 `fs.readFile()`나 `setTimeout()` 같은 비동기 함수를 만나면, 해당 작업은 `libuv`에게 위임됩니다.
+`libuv`는 위임받은 비동기 작업을 처리하기 위해 다음과 같이 결정합니다. + 1. OS 커널 지원 확인: `libuv`는 운영체제 커널이 해당 작업을 네이티브 비동기 메커니즘으로 지원하는지 확인합니다. 네트워크 I/O(예: 데이터베이스 쿼리, HTTP 요청)처럼 지원되는 작업은 커널에게 요청하고, 워커 스레드 풀을 사용하지 않습니다. + 2. 스레드 풀 사용: 커널이 비동기를 지원하지 않거나 시간이 오래 걸리는 작업(블로킹 위험이 있는 작업)은 `libuv`가 관리하는 워커 스레드 풀(Thread Pool)에 위임됩니다.
+ ◦ 사용되는 작업 예시: 파일 시스템(fs 모듈의 동기 작업 제외), DNS 조회, `crypto.pbkdf2()`와 같은 고비용 암호화 함수, `Zlib` 작업 등이 스레드 풀을 사용합니다.
+ ◦ 스레드 풀 크기: `libuv`는 기본적으로 4개의 스레드를 가진 스레드 풀을 생성합니다. `UV_THREADPOOL_SIZE` 환경변수로 최대 1024개까지 늘릴 수 있습니다. (`libuv` 1.30.0 기준) +### 4. 콜 스택 비우기 +동기 코드가 모두 실행되면 콜 스택은 비워집니다. +### 5. 이벤트 루프의 순환 6단계 (매크로태스크) +동기 코드 실행이 완료되고 콜 스택이 비면, 이벤트 루프는 6개의 페이즈(Phase)를 순서대로 방문하며 각 페이즈가 관리하는 매크로태스크 큐의 콜백을 실행합니다. +
(페이즈에서 페이즈로 넘어가는 것을 `Tick`이라고 합니다.) + 1. **Timer Phase (타이머 단계)**: `setTimeout`, `setInterval` 타이머 만료 콜백을 처리합니다.최소 힙 기반으로 관리됩니다. + 2. **Pending Callbacks Phase (보류 중인 콜백 단계)**: 이전 루프에서 시스템 한도 등으로 인해 처리되지 못한 I/O 콜백이나 일부 시스템 콜백을 처리합니다. + 3. **Idle, Prepare Phase (유휴, 준비 단계)**: Node.js 내부 관리를 위한 단계로, JavaScript 코드를 실행하지 않습니다. + 4. **Poll Phase (폴링 단계)**
+ ◦ 새로운 I/O 이벤트를 가져오고 콜백을 실행합니다. 파일 I/O 완료, 네트워크 응답 도착 등 대부분의 I/O 콜백이 실행됩니다.
+ ◦ 큐가 비어있으면 다음 페이즈로 이동하기 전 일정 시간 대기할 수 있습니다. + 5. **Check Phase (확인 단계)**: `setImmediate()` 콜백을 처리합니다. + 6. **Close Callbacks Phase (종료 콜백 단계)**: `socket.on('close', ...)`와 같은 close 이벤트 핸들러를 처리합니다. + +## 마이크로태스크 큐 +이벤트 루프에 속하지는 않지만, 비동기 실행 순서에서 마이크로태스크(Microtask)는 매크로태스크보다 높은 우선순위를 가집니다.
+(마이크로태스크 큐는 `libuv`가 아닌 Node.js 런타임 내부에 독립적으로 구현되어 있습니다.)
+현재 실행 중인 매크로태스크가 완료된 직후, 이벤트 루프가 다음 단계(Phase)로 넘어가기 전에 완전히 비워집니다. + +1. `process.nextTick` Queue (nextTickQueue): 모든 마이크로태스크 중 가장 높은 우선순위를 가집니다. +2. Promise Queue (microTaskQueue): `Promise.then()`, `.catch()`, `.finally()` 콜백을 담고 있으며, `process.nextTick` 다음으로 실행됩니다. + +이러한 동작 원리 덕분에 Node.js는 싱글 스레드의 한계를 극복하고 효율적인 비동기 처리를 수행할 수 있습니다. \ No newline at end of file diff --git a/docs/posts/react/react-race-condition.mdx b/docs/posts/react/react-race-condition.mdx new file mode 100644 index 0000000..79adbb1 --- /dev/null +++ b/docs/posts/react/react-race-condition.mdx @@ -0,0 +1,220 @@ +--- +title: 'React에서 Race Condition 방지하기' +type: 'concept' +language: 'React' +tags: + - React + - Race Condition +dateModified: 2025.09.24 +--- + +# React에서 Race Condition 방지하기 + +## Race Condition + +Race Condition은 두 개 이상의 비동기 작업이 동시에 실행되면서 예상치 못한 결과가 적용되어 버그나 비일관성을 만드는 상황을 말합니다. + +서버 개발 혹은 네트워크 프로그래밍에서 주로 다루는 개념이나, 프론트엔드 환경에서도 발생할 수 있는 상황입니다. + +비동기 데이터 패칭과 상태 업데이트가 빈번하게 일어나는 프론트엔드 환경에서도 race condition을 적절히 방지하는 것이 중요합니다. + +
+ +## 프론트엔드에서 Race Condition이 발생하는 상황 예시 + +#### 1. 최신 데이터가 아닌 데이터가 노출되는 검색창 + +```js +function SearchComponent() { + const [query, setQuery] = useState(''); + const [result, setResult] = useState(''); + + useEffect(() => { + if (!query) return; + + // query가 바뀔 때마다 새로운 fetch 요청 발생 + fetch(`/api/search?q=${query}`) + .then((res) => res.json()) + .then((data) => { + // 응답이 도착하면 결과 업데이트 + setResult(data.result); + }); + }, [query]); + + return ( +
+ setQuery(e.target.value)} placeholder="검색어 입력" /> +

검색 결과: {result}

+
+ ); +} +``` + +위와 같이 코드를 작성했을 때, query가 바뀔 때마다 요청하는 데이터의 응답 속도는 네트워크 환경에 따라 달라지게 됩니다. + +처음 입력한 검색어 '안녕' 보다 '안'의 요청에 대한 응답이 늦게 도착하는 경우, 최신 검색어인 '안녕'에 대한 결과가 아닌 '안'에 대한 결과가 화면에 표시됩니다. + +즉, 사용자에게 엉뚱한 결과가 노출되는 race condition이 발생합니다. + +#### 2. 페이지 전환 시 이전 페이지 데이터가 늦게 도착하는 경우 + +```js +function UserPage({ userId }) { + const [user, setUser] = useState(null); + + useEffect(() => { + fetch(`/api/user/${userId}`) + .then((res) => res.json()) + .then((data) => setUser(data)); + }, [userId]); + + return ( +
+

사용자 정보

+
{JSON.stringify(user, null, 2)}
+
+ ); +} +``` + +위 상황에서 사용자가 userId=1 → userId=2로 빠르게 이동하면, +/api/user/2 요청보다 /api/user/1 응답이 늦게 도착해 화면에 잘못된 유저 정보가 나타날 수 있습니다. + +
+ +## React 환경에서 Race Condition 방지 방법 + +#### 1. useEffect의 cleanUp 함수 사용하기 + +React의 useEffect 내부에서는 콜백으로 직접 비동기 함수를 넣을 수 없습니다. + +```js +// ❌ 잘못된 코드 +//'effect callbacks are synchronous to prevent race conditions. put the async function inside' 에러 발생 + +useEffect(async () => { + const res = await fetch('/api/data'); + const json = await res.json(); + setData(json); +}, []); +``` + +이는 React 팀에서 race condition 방지를 위해 의도적으로 막아둔 것입니다. + +useEffect 콜백 자체는 동기적이어야 하며, 비동기 처리는 내부에서 따로 호출해야 합니다. + +```js +// ✅ 올바른 코드 +useEffect(() => { + let ignore = false; + + async function fetchData() { + const res = await fetch('/api/data'); + const json = await res.json(); + if (!ignore) setData(json); // 최신 요청만 반영 + } + + fetchData(); + + return () => { + ignore = true; // cleanup으로 이전 요청 무시 + }; +}, [id]); +``` + +위와 같이 useEffect의 콜백함수 내부에 비동기 함수를 넣어서 사용할 수 있습니다. + +다만 useEffect 내에 비동기 함수가 존재하면 내부에서 비동기 함수가 생성, 실행을 반복하므로 이전 비동기 함수에 대한 처리를 해주어야 합니다. + +여기서는 ignore 플래그를 사용해 클린업 시점 이후에 도착하는 응답은 무시하도록 했습니다. + +#### 2. Abort Controller로 이전 요청 취소하기 + +fetch API는 AbortController를 지원합니다. + +이를 이용하면 컴포넌트가 언마운트되거나 의존성이 바뀔 때 이전 요청 자체를 취소할 수 있습니다. + +```js +useEffect(() => { + const controller = new AbortController(); + + async function fetchData() { + try { + const res = await fetch(`/api/search?q=${query}`, { + signal: controller.signal, + }); + const json = await res.json(); + setResult(json); + } catch (err) { + if (err.name !== 'AbortError') { + console.error(err); + } + } + } + + fetchData(); + + // cleanup 단계에서 이전 요청 취소 + return () => { + controller.abort(); + }; +}, [query]); +``` + +이렇게 하면 query가 바뀔 때마다 이전 요청은 자동 취소되므로, 뒤늦은 응답이 화면에 반영되지 않습니다. + +#### 3. unique id로 최신 요청만 반영하기 + +비동기 요청마다 고유한 ID(ex: 타임스탬프)를 부여하고, 응답 시점에 '가장 최신 요청인지' 확인 후 상태를 반영합니다. + +```js +const lastRequestId = useRef(0); + +useEffect(() => { + const requestId = Date.now(); + lastRequestId.current = requestId; + + async function fetchData() { + const res = await fetch(`/api/search?q=${query}`); + const json = await res.json(); + + if (lastRequestId.current === requestId) { + setResult(json); + } + } + + fetchData(); +}, [query]); +``` + +이 방식은 AbortController를 지원하지 않는 환경에서 유용합니다. + +#### 4. 데이터 페칭 라이브러리 사용하기 + +React Query, SWR과 같은 데이터 패칭 라이브러리를 사용할 수 있습니다. + +이러한 라이브러리들은 요청 중복/취소/최신화 관리 기능을 내장하고 있어 race condition의 위험을 줄여줍니다. + +```js +// React Query 예시 +import { useQuery } from '@tanstack/react-query'; + +function Search({ query }) { + const { data, isLoading } = useQuery({ + queryKey: ['search', query], + queryFn: () => fetch(`/api/search?q=${query}`).then((res) => res.json()), + enabled: !!query, // query가 있을 때만 실행 + }); + + if (isLoading) return

로딩 중...

; + return

검색 결과: {data.result}

; +} +``` + +
+ +## 정리 + +- Race condition은 프론트엔드에서도 빈번히 발생하는 문제로, 올바른 관리 없이는 사용자 경험이 깨지거나 심각한 버그로 이어질 수 있으므로 항상 고려해야 합니다. + +- React에서는 AbortController, 요청 식별자(unique id) 관리, 혹은 React Query/SWR 같은 라이브러리를 통해 효과적으로 방지 가능합니다. diff --git a/package.json b/package.json index b793ac7..0f677e2 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", + "react-resizable-panels": "^3.0.6", "react-router-dom": "^7.4.1" }, "devDependencies": { diff --git a/public/docs-assets/database/dirty-read.png b/public/docs-assets/database/dirty-read.png new file mode 100644 index 0000000..b587dd8 Binary files /dev/null and b/public/docs-assets/database/dirty-read.png differ diff --git a/public/docs-assets/database/non-repeatable-read.png b/public/docs-assets/database/non-repeatable-read.png new file mode 100644 index 0000000..961088d Binary files /dev/null and b/public/docs-assets/database/non-repeatable-read.png differ diff --git a/public/docs-assets/database/phantom-read.png b/public/docs-assets/database/phantom-read.png new file mode 100644 index 0000000..1144778 Binary files /dev/null and b/public/docs-assets/database/phantom-read.png differ diff --git a/public/docs-assets/network/DNS-1.png b/public/docs-assets/network/DNS-1.png new file mode 100644 index 0000000..c5ad821 Binary files /dev/null and b/public/docs-assets/network/DNS-1.png differ diff --git a/public/docs-assets/network/DNS-2.png b/public/docs-assets/network/DNS-2.png new file mode 100644 index 0000000..3eadcd6 Binary files /dev/null and b/public/docs-assets/network/DNS-2.png differ diff --git a/public/manifest.json b/public/manifest.json index afbccd7..0f71482 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -3,7 +3,7 @@ "name": "디룩(Delook)", "author": "@waterbinnn", "homepage_url": "https://www.delook.co.kr/", - "version": "1.0.9", + "version": "1.0.10", "description": "개념 학습부터 기술 면접 준비까지, 성장하는 개발자의 새 탭", "action": { "default_popup": "popup.html", diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 2502dd1..cb0089a 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -14,7 +14,7 @@ export function Layout() {
diff --git a/src/components/layout/SidebarLayout.tsx b/src/components/layout/SidebarLayout.tsx index 72f7d06..915c8a7 100644 --- a/src/components/layout/SidebarLayout.tsx +++ b/src/components/layout/SidebarLayout.tsx @@ -1,6 +1,13 @@ import { useIsMobile } from '@/hooks'; -import { Sidebar, SidebarInset, SidebarMenu, SidebarProvider, SidebarTrigger } from '../ui'; +import { + Sidebar, + SidebarInset, + SidebarMenu, + SidebarProvider, + SidebarRail, + SidebarTrigger, +} from '../ui'; export const SidebarLayout = ({ sidebarMenu, @@ -13,14 +20,16 @@ export const SidebarLayout = ({ return ( - + {sidebarMenu} + -
+
{isMobile && ( )} + {children}
diff --git a/src/components/ui/Resizable.tsx b/src/components/ui/Resizable.tsx new file mode 100644 index 0000000..bc1e0ff --- /dev/null +++ b/src/components/ui/Resizable.tsx @@ -0,0 +1,40 @@ +import { GripVertical } from 'lucide-react'; +import * as ResizablePrimitive from 'react-resizable-panels'; + +import { cn } from '@/lib/utils'; + +const ResizablePanelGroup = ({ + className, + ...props +}: React.ComponentProps) => ( + +); + +const ResizablePanel = ResizablePrimitive.Panel; + +const ResizableHandle = ({ + withHandle, + className, + ...props +}: React.ComponentProps & { + withHandle?: boolean; +}) => ( + div]:rotate-90', + className, + )} + {...props} + > + {withHandle && ( +
+ +
+ )} +
+); + +export { ResizableHandle, ResizablePanel, ResizablePanelGroup }; diff --git a/src/components/ui/Sidebar.tsx b/src/components/ui/Sidebar.tsx index 0cd4326..811fe70 100644 --- a/src/components/ui/Sidebar.tsx +++ b/src/components/ui/Sidebar.tsx @@ -17,6 +17,8 @@ import { TooltipTrigger, } from '@/components'; import { useIsMobile } from '@/hooks'; +import { useSidebarResize } from '@/hooks/useSidebarResize'; +import { mergeButtonRefs } from '@/lib/merge-button-refs'; import { cn } from '@/lib/utils'; const SIDEBAR_COOKIE_NAME = 'sidebar_state'; @@ -25,6 +27,8 @@ const SIDEBAR_WIDTH = '22rem'; const SIDEBAR_WIDTH_MOBILE = '18rem'; const SIDEBAR_WIDTH_ICON = '3rem'; const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; +const MAX_SIDEBAR_WIDTH = '25rem'; +const MIN_SIDEBAR_WIDTH = '18rem'; export type SidebarContextProps = { state: 'expanded' | 'collapsed'; @@ -34,6 +38,10 @@ export type SidebarContextProps = { setOpenMobile: (open: boolean) => void; isMobile: boolean; toggleSidebar: () => void; + width: string; + setWidth: (width: string) => void; + isDraggingRail: boolean; + setIsDraggingRail: (isDraggingRail: boolean) => void; }; const SidebarContext = React.createContext(null); @@ -53,6 +61,7 @@ const SidebarProvider = React.forwardRef< defaultOpen?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; + defaultWidth?: string; } >( ( @@ -63,12 +72,15 @@ const SidebarProvider = React.forwardRef< className, style, children, + defaultWidth = SIDEBAR_WIDTH, ...props }, ref, ) => { const isMobile = useIsMobile(); + const [width, setWidth] = React.useState(defaultWidth); const [openMobile, setOpenMobile] = React.useState(false); + const [isDraggingRail, setIsDraggingRail] = React.useState(false); // This is the internal state of the sidebar. // We use openProp and setOpenProp for control from outside the component. @@ -120,8 +132,22 @@ const SidebarProvider = React.forwardRef< openMobile, setOpenMobile, toggleSidebar, + width, + setWidth, + isDraggingRail, + setIsDraggingRail, }), - [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], + [ + state, + open, + setOpen, + isMobile, + openMobile, + // setOpenMobile, + toggleSidebar, + width, + isDraggingRail, + ], ); return ( @@ -130,7 +156,7 @@ const SidebarProvider = React.forwardRef<
{ - const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + const { isMobile, state, openMobile, setOpenMobile, isDraggingRail } = useSidebar(); if (collapsible === 'none') { return ( @@ -234,28 +260,33 @@ const Sidebar = React.forwardRef< data-collapsible={state === 'collapsed' ? collapsible : ''} data-variant={variant} data-side={side} + data-dragging={isDraggingRail} > {/* This is what handles the sidebar gap on desktop */}