최신글

CS

[CS 스터디] 2주차 메모리 관리 + 가상 메모리

[2주차] 메모리 관리 (페이징 & 세그멘테이션) + 가상 메모리 주소 공간, 물리적 주소, 논리적 주소, 주소 바인딩 메모리 관리 기법 (페이징, 세그멘테이션) 가상 메모리 (Demand Paging, Page Fault) 캐시 메모리, TLB(Translation Lookaside Buffer) 메모리 할당 전략 (First Fit, Best Fit, Worst Fit) 🔵 주소 공간, 물리적 주소, 논리적 주소, 주소 바인딩 주소 공간(Address Space) 컴퓨팅에서 주소 공간은 물리 메모리나 가상 메모리 등 다른 논리적 실체나 물리적 실체에 대응되는 주소의 범위를 정의한 공간을 말한다. 메모리 주소는 컴퓨터 메모리의 물리 위치를 파악하며, 데이터가 저장되는 위치를 가리킨다. 프로세스와 주소 공간 일반적으로 운영체제는 하나의 프로세스에 대하여 하나의 주소 공간을 제공하며, 프로세스 내의 사용자 스레드들은 주소공간을 공유한다. 물리적 주소(Physical Space) 메모리는 배열이기 때문에 인덱스 값을 가지는데, 이 인덱스 값을 '물리적 주소'라고 한다. 물리적 주소는 메모리 자체의 인덱스다. 논리적 주소(Logical Space) 논리적 주소는 CPU 입장에서의 메모리 주소, 또는 프로그램 실행 중에 CPU가 생성하는 주소이다. 따라서 가상 주소라고도 한다. CPU가 명령어를 실행하면서 생성하는 주소이다. 사용자 프로세스가 접근하는 주소는 모두 논리적 주소 변환 작업은 MMU(Memory Management Unit)에서 이루어진다. 논리적 주소와 물리적 주소의 관계 | 종류 | 의미 | 생성 주체 | 접근 방식 | | ----------- | -------------------------- | ---------- | ----------------------------- | | 논리적 주소 | CPU가 생성하는 가상의 주소 | CPU | 사용자 프로세스에서 직접 사용 | | 물리적 주소 | 실제 메모리 주소 | MMU가 계산 | 운영체제나 하드웨어에서 접근 | 논리적 주소가 필요한 이유 메모리 보호: 각 프로세스는 자신의 주소 공간만 보게 하여 다른 프로세스의 메모리를 침범하지 못하도록 한다. 멀티프로그래밍 지원: 서로 다른 프로세스들이 각자 독립된 주소 공간을 갖도록 함 프로그램의 이식성: 논리 주소를 사용하면 프로그램을 어느 메모리의 어느 위치에서든 실행할 수 있다. 주소 바인딩(Address Binding) 프로세스가 논리적 주소를 참조하기 때문에 실제 메모리에 데이터를 읽고 쓰기 위해서는 해당 논리적 주소를 물리적 주소로 매핑하는 과정이 필요하다. 이를 주소 바인딩이라고 한다. 주소 바인딩은 프로세스가 메모리에 접근할 수 있도록 하는 중요한 단계 MMU MMU 는 CPU와 메모리 사이에 존재하는 하드웨어 장치로, 논리적 주소 -> 물리적 주소 변환을 수행한다. 주요 역할 주소 변환: CPU가 생성하는 논리 주소에 Base Register 값을 더해 실제 물리 주소 계산 메모리 보호: Bounds Register를 통해 접근 가능한 메모리 범위 제한 🔵 메모리 관리 기법 (페이징, 세그멘테이션) 페이징(Paging) 이란 페이징은 가상 주소 공간과 물리적 주소 공간을 동일한 크기의 고정 블록으로 나누어 매핑하는 메모리 관리 기법이다. 가상 주소 공간을 나눈 고정 크기 블록 -> 페이지(Page) 물리 주소 공간을 나눈 고정 크기 블록 -> 프레임(Frame) 페이징은 내부 단편화 발생 가능 세그멘테이션(Segmentation) 이란 세그멘테이션은 프로그램의 논리적 구조에 따라 주소 공간을 여러개의 세그먼트로 나누어 메모리를 관리하는 방식이다. 세그먼트는 코드(Code), 데이터(Data), 스택(Stack)과 같이 의미 있는 논리 단위로 나뉜다. 각 세그먼트는 서로 다른 크기를 가질 수 있으며, 연속적인 메모리 공간을 할당받는다. 주소는 (세그먼트 번호, 오프셋) 형태로 표현한다. 세그멘테이션은 외부 단편화 발생 가능 🔵 가상 메모리 (Demand Paging, Page Fault) 가상 메모리(Virtual Memory) 실제 메모리 크기보다 요구 메모리가 큰 프로그램을 실행하기 위해 사용하는 메모리 관리 기법이다. 모든 데이터를 주 기억장치에 올리지 않고, 필요한 것들만 올려서 사용한다. 나머지는 디스크(보조 기억 장치)에 보관되며, 필요할 때만 메모리로 가져온다. CPU는 모든 주소를 가상 주소로 인식하고, 실제 주소는 MMU가 변환해서 사용한다. 요구 페이징(Demand Paging) 요구 페이징은 프로세스의 페이지 중 실제로 필요한 페이지만 메모리에 올리고, 나머지는 요청이 있을 때 디스크에서 가져오는 방식이다. 처음엔 메모리에 아무것도 올리지 않음 CPU가 요청한 주소가 없는 경우(Page Fault) 발생 이때 해당 페이지를 디스크에서 RAM으로 로드 페이지 폴트(Page Fault) 페이지 폴트(Page Fault)는 프로세스가 접근하려는 메모리 페이지가 물리적 메모리(RAM)에 존재하지 않을 때 발생하는 이벤트이다. 페이지 폴트 발생: 프로세스가 메모리에 접근하려 할 때, 해당 가상 주소에 대응하는 페이지가 물리적 메모리에 없으면 페이지 폴트가 발생, 인터럽트 처리: 페이지 폴트는 운영 체제에 의해 처리되는 인터럽트. 운영 체제는 이 인터럽트를 받고 현재 CPU의 상태를 저장한 후 페이지 폴트 처리 루틴을 실행. 페이지 로딩: 운영 체제는 필요한 페이지를 찾아 물리적 메모리로 로드함. 이 페이지는 디스크의 스왑 영역이나 해당 파일 시스템에서 가져올 수 있음. 페이지 테이블 업데이트: 페이지가 메모리에 로드된 후, 페이지 테이블이 업데이트되어 새로운 매핑 정보를 반영. 프로세스 재개: 페이지 로딩이 완료되면, CPU는 원래의 프로세스를 재개. 🔵 캐시 메모리, TLB(Translation Lookaside Buffer) 캐시 메모리 (Cache Memory) 캐시 메모리(Cache Memory) 는 CPU와 주기억장치(RAM) 사이에 위치한 고속의 임시 저장장치로, 자주 사용하는 데이터를 빠르게 접근하기 위해 사용된다. • CPU가 자주 접근하는 데이터를 임시로 저장하여 속도 향상 • 작지만 빠름, 용량은 작고, 가격은 비쌈 | 계층 | 설명 | | ------- | --------------------------------------------------- | | L1 캐시 | 가장 빠르고 CPU 내부에 위치. 용량 작음 | | L2 캐시 | CPU 와 메모리 사이에 위치. L1 보다 느리지만 용량 큼 | | L3 캐시 | 일부 고성능 CPU에서 제공. L2 보다 크고 느림 | TLB (Translation Lookaside Buffer) TLB(Translation Lookaside Buffer) 는 가상 주소 → 물리 주소 변환 시 사용되는 페이지 테이블 캐시이다. • MMU 내부에 존재하며, 최근 참조한 페이지 테이블 항목을 저장 • 페이지 테이블 접근 시간을 줄여주는 역할 작동 과정 1. CPU가 가상 주소 접근 2. MMU가 TLB에서 먼저 검색 3. 있다면 → 바로 물리 주소 반환 (TLB hit) 4. 없다면 → 페이지 테이블 접근 (TLB miss) 후 TLB에 등록 효과 • TLB hit → 주소 변환 속도 빠름 • TLB miss → 페이지 테이블 접근 → 속도 저하 🔵 메모리 할당 전략 (First Fit, Best Fit, Worst Fit) 메모리 공간을 요청된 크기에 따라 어떻게 효율적으로 배분할 것인가에 대한 전략 First Fit (최초 적합) • 가장 먼저 발견한 충분한 공간에 할당 • 빠르지만, 할당된 공간 이후에 작은 공간들이 남아 외부 단편화 발생 가능 Best Fit (최적 적합) • 가장 크기가 딱 맞는 공간에 할당 (남는 공간이 가장 적은 곳) • 공간 낭비가 적지만, 탐색 시간이 길 수 있음 • 외부 단편화가 많이 발생 Worst Fit (최악 적합) • 가장 큰 공간에 할당하여 남은 공간이 충분히 크도록 함 • 큰 공간을 계속 쪼개므로 큰 프로세스가 들어갈 공간이 사라질 수 있음

2025년 03월 31일 09:11

CS

[CS 스터디] 1주차 운영체제 개요 + 프로세스 & 스레드

[1주차] 운영체제 개요 + 프로세스 & 스레드 컴퓨터 구성 요소 운영체제의 역할 커널(User Mode vs Kernel Mode) 프로세스의 구조 프로세스 vs 스레드 멀티프로세싱(Multiprocessing) 멀티스레딩(Multithreading) 컨텍스트 스위칭 🔵 컴퓨터 구성 요소: Top-Level View 컴퓨터 시스템은 CPU, 메모리, I/O 모듈이 System Bus 를 통해 연결되어 있으며, 운영체제는 이 자원들을 효율적으로 관리한다. 주요 구성 요소 CPU (Central Processing Unit) PC: 다음에 실행할 명령어의 주소 IR: 현재 실행 중인 명령어 MAR: 접근할 메모리 주소 MBR: 메모리에서 읽거나 쓸 데이터 I/O AR, I/O BR: 입출력 주소 및 버퍼 Execution Unit: 실제 연산 수행 Main Memory 명령어와 데이터를 저장하는 공간 주소를 통해 위치 지정 I/O Module CPU와 장치 간 데이터 전달 버퍼로 속도 차 조절 System Bus 주소 버스, 데이터 버스, 제어 버스로 구성 🔵 운영체제의 역할 운영체제는 하드웨어 자원(CPU, 메모리, 파일 등)을 효율적이고 안정적으로 관리하는 시스템 소프트웨어이다. 주요 역할은 다음과 같다: 프로세스 관리 운영체제는 여러 프로세스가 공정하게 CPU를 사용할 수 있도록 스케줄링하고, 상태를 추적하며, 컨텍스트 스위칭을 통해 프로세스를 전환한다. 프로세스 상태와 전이 New: 생성 중 Ready: 실행 준비 완료, 대기 중 Running: 명령어 실행 중 Blocked: 이벤트 대기 중 Exit: 종료됨 상태 전이 예시 Admit: New → Ready Dispatch: Ready → Running Timeout: Running → Ready Event Wait: Running → Blocked Event Occurs: Blocked → Ready Release: Running → Exit PCB (Process Control Block) 운영체제는 각 프로세스를 추적하기 위해 PCB를 사용한다. PCB는 다음 정보를 포함한다 | 항목 | 설명 | | --------------- | ----------------------------- | | Process State | 현재 상태 (Ready, Running 등) | | Process ID | 고유한 식별자 | | Program Counter | 다음 실행할 명령어 주소 | | CPU Registers | 레지스터 값 저장 | | Memory Info | 메모리 주소 범위 등 | | I/O Info | 열려 있는 파일/디바이스 등 | 운영체제의 프로세스 관리 구조 운영체제는 단순히 PCB 하나로만 프로세스를 관리하는 것이 아니라, 메모리 / 입출력 / 파일 시스템에 대한 정보를 포함한 다양한 제어 테이블을 함께 사용한다. 위 그림은 OS가 내부적으로 유지하는 Control Tables 구조를 나타낸 것이다. Memory Tables: 각 프로세스가 사용하는 메모리 영역과 접근 권한 정보 저장 I/O Tables: 어떤 장치를 어느 프로세스가 사용하는지 정보 기록 File Tables: 열려 있는 파일, 파일 포인터, 접근 모드 등 저장 Primary Process Table: 현재 실행 중인 모든 프로세스들의 PCB를 보관 이러한 구조는 링크드 리스트 형태로 구성되며, 운영체제가 동시에 실행 중인 프로세스를 효율적으로 관리하기 위해 사용된다. 각 프로세스는 독립된 Process Image를 가지고 있으며, 이는 코드, 데이터, 스택, PCB 정보를 포함한 프로세스의 전체 상태를 의미한다. 메모리 관리 운영체제는 프로세스마다 메모리 공간을 분리하고 보호하며, 가상 메모리를 통해 더 큰 주소 공간을 제공한다. 주소 변환(MAR, MBR) 프로세스 간 메모리 보호 물리 메모리 ↔ 가상 메모리 관리 파일 시스템 관리 운영체제는 파일 생성/삭제, 이름 지정, 접근 권한 등 파일 관련 기능을 제공한다. 파일 시스템은 디스크 상의 데이터에 대한 추상화 계층을 제공하며, 사용자가 파일을 디렉토리 구조로 관리할 수 있도록 한다. 🔵 커널(User Mode vs Kernel Mode) 커널이란? 커널은 운영체제의 핵심으로, 프로세스, 메모리, 파일, I/O 등의 자원을 직접 제어함 사용자 프로그램은 커널을 직접 조작할 수 없고, 시스템 콜(System Call) 을 통해 간접적으로 요청함 쉘(Shell)이란? 쉘은 사용자와 커널 사이의 인터페이스 사용자가 명령어를 입력하면, 쉘은 이를 시스템 콜로 번역해 커널에 전달함 크게 두 종류 - Command-line Shell: bash, zsh 등 - Graphical Shell: Windows Explorer, macOS Finder 등 GUI 기반 User Mode vs Kernel Mode | 구분 | User Mode | Kernel Mode | | --------- | ----------------------------- | ------------------------------------ | | 권한 수준 | 제한적 | 모든 하드웨어 자원 접근 가능 | | 코드 실행 | 사용자 애플리케이션 | 운영체제 커널 코드 | | 예시 | 일반 앱, 프로세스 | 시스템 콜 처리, 드라이버 실행 등 | | 전환 방법 | 시스템 콜 (trap), 인터럽트 등 | 사용자 프로그램에 제어 반환 (return) | 사용자 프로그램이 시스템 자원을 사용하려면 커널 모드로 전환이 필요함 커널의 주요 역할 커널은 항상 메모리에 상주하는 운영체제의 핵심이 되는 부분입니다. 컴퓨터 자원을 관리하는 자원 관리자로서 대표적으로 다음 4가지 기능을 가지고 있다. 커널은 사용자가 물리적인 하드웨어에 접근하고 사용할 수 있도록 하기 위한 목적을 가지고 있고, 사용자가 쉘(Shell)을 통해 입력한 명령어를 해석하여 하드웨어에 전달해주는 역할을 한다. 메모리 관리 각 프로그램이 어디에서, 무엇을, 얼마나 사용하는지를 추적하고. 메모리 자원을 할당하는 역할을 한다. 가상 메모리를 사용할 수 있도록 한다. 프로세스 관리 및 CPU 스케쥴링 사용자가 시스템에 로그인 함과 동시에 수많은 프로세스가 실행되는데, 커널은 CPU의 시간 자원을 배분하는 역할 - 어떤 프로세스가 언제, 얼마나 사용할지 - 을 하여 여러 개의 프로세스가 동시에 동작하는 것처럼 보이게 합니다. 디바이스 관리 컴퓨터에 연결된 장치들을 드라이버라는 매개체를 통해서 제어하고 관리한다. 시스템 콜 인터페이스 및 보안 시스템 콜을 제공하여 응용 프로그램 - 프로세스의 서비스 요청을 수신한다. 커널 구조 유형 | 구조 | 특징 | 예시 운영체제 | | ------------- | ---------------------------------------- | -------------- | | 모놀리식 커널 | 모든 기능이 하나의 커널 내에 포함 | Linux, Unix | | 마이크로커널 | 최소 기능만 커널에, 나머지는 사용자 공간 | Minix, QNX | | 하이브리드 | 두 구조의 장점을 절충 | Windows, macOS | 시스템 콜 예시 fork() : 새로운 프로세스 생성 exec() : 다른 프로그램 실행 read(), write() : 파일 I/O kill() : 프로세스 종료 요청 🔵 프로세스의 구조 프로세스 vs 프로그램 프로그램: 하드디스크에 저장된 정적인 명령어 집합 프로세스: 실행 중인 프로그램, 메모리에 적재된 상태 하나의 컴퓨터에서 여러 프로세스가 동시에 실행될 수 있으며, 각 프로세스는 자신만의 코드, 데이터, 스택, 레지스터를 가진다. 프로세스는 메모리에 다음과 같은 형태로 구성된다. 이를 프로세스 이미지 (Process Image) 라고 하며, 운영체제는 이 전체 상태를 관리한다. | 영역 | 설명 | | -------------- | --------------------------------------------------------------------------------------------------- | | PCB | 프로세스의 상태를 저장하는 커널 영역 | | Text(Code) | 실행할 프로그램의 명령어 (코드 영역)→ Private User Address Space 내부에 포함됨 | | Data | 전역 변수(static 변수 포함) 저장→ Private User Address Space 내부에 포함됨 | | Heap | 동적으로 할당되는 메모리 영역 (e.g., malloc)→ Private User Address Space 내부에 포함됨 | | Stack | 함수 호출 시 지역 변수, 리턴 주소 등이 저장되는 영역→ User Stack과 Kenel Stack 두 부분으로 나뉨 | 운영체제는 이 정보를 기반으로 프로세스를 메모리에 적재하고, 필요한 시점에 스케줄링/복원/삭제 등을 수행한다. 🔵 프로세스 vs 스레드 프로세스(Process) 자원 소유의 단위 (Unit of Resource Ownership) 스케줄링/실행의 단위 (Unit of Execution) 하나의 프로세스는 고유한 코드, 데이터, 스택, PCB를 가진다. 운영체제는 각 프로세스를 독립적으로 관리한다. 스레드(Thread) 실행의 단위만 분리됨 (Dispatching Unit) 자원은 프로세스와 공유, 스택은 스레드마다 개별 하나의 프로세스 내에서 여러 스레드가 생성되어 실행될 수 있음 즉, 자원은 프로세스 단위로 소유, 실행은 스레드 단위로 분화됨 프로세스 vs 스레드 비교 | 항목 | 프로세스 | 스레드 | | ------------------ | ------------------------------------- | ------------------------------------- | | 자원 소유 | 독립적으로 가짐 | 동일 프로세스 내 자원 공유 | | 실행 단위 | 독립적인 실행 흐름 | 프로세스 내의 실행 흐름 | | 메모리 공간 | 각각 독립적 | 코드/데이터 영역은 공유, 스택만 개별 | | 문맥 교환 비용 | 크다 | 작다 | | 생성/종료 오버헤드 | 큼 | 작음 | | 안정성 | 하나가 죽어도 다른 프로세스 영향 없음 | 하나가 죽으면 전체 프로세스 영향 가능 | 스레드의 장점과 단점 장점 자원 공유로 문맥 교환 비용이 작음 빠른 통신 및 협업 가능 (shared memory) 병렬 처리를 통해 멀티코어 활용 효율적 단점 동기화 문제(Race condition) 발생 가능 하나의 스레드 오류로 전체 프로세스가 중단될 수 있음 디버깅과 유지보수가 복잡함 🔵 멀티프로세싱(Multiprocessing) 여러 개의 프로세스를 생성하여 동시에 실행하는 방식 각 프로세스는 독립적인 주소 공간과 자원을 가짐 일반적으로 멀티코어 CPU에서 병렬 처리를 위해 사용됨 하나의 CPU에서 멀티프로세싱을 할 경우는 시분할 방식(time sharing), 멀티코어 CPU에서는 진짜 동시 실행(parallel execution)이 가능 🔵 멀티스레딩(Multithreading) 정의 하나의 프로세스 내에서 여러 실행 흐름(스레드) 을 가지는 것 각 스레드는 독립적인 실행 스택을 가지지만, 공통 주소 공간을 공유함 운영체제별 스레드 지원 방식 | 운영체제 | 설명 | | -------------------- | ------------------------------------------------------------ | | MS-DOS | 하나의 프로세스에 하나의 스레드만 지원 | | UNIX (old) | 여러 프로세스는 가능, 하지만 프로세스당 1스레드 | | Modern UNIX 계열 | 하나의 프로세스에서 여러 스레드 지원 (ex. Linux, Solaris 등) | 스레드의 구조 하나의 스레드는 다음 정보를 가짐 Execution State: running, ready 등 Thread Context: 스레드 실행 시 CPU 레지스터 저장 Execution Stack: 지역 변수 저장 Static Storage for Local Variables Memory/Resource 접근: 같은 프로세스의 스레드끼리는 공유 즉, 프로그램 코드/데이터는 공유하고, 스택과 context는 개별 멀티프로세싱 vs 멀티스레딩 | 항목 | 멀티프로세싱 | 멀티스레딩 | | -------------- | ------------------------------ | -------------------------------- | | 실행 단위 | 여러 프로세스 | 하나의 프로세스 내 여러 스레드 | | 주소 공간 | 각각 독립 | 공유됨 | | 메모리 사용량 | 크다 | 상대적으로 작다 | | 문맥 교환 비용 | 크다 | 작다 | | 통신 방식 | IPC (Pipe, Socket 등) | 공유 메모리 | | 안정성 | 하나가 죽어도 나머지 영향 없음 | 하나가 죽으면 전체 프로세스 위험 | 멀티프로세싱 예시: 웹 서버에서 여러 클라이언트를 처리할 때, 아예 독립된 서버 인스턴스를 띄우는 방식 멀티스레딩 예시: 게임 프로그램에서 그래픽 처리, 사운드, 물리 엔진을 각각 스레드로 처리 싱글 스레드 vs 멀티 스레드 모델 Single Threaded Process - PCB + User Stack + Kernel Stack + User Address Space Multithreaded Process - 하나의 PCB + 여러 TCB + 각 스레드별 스택 - 코드/데이터/주소 공간은 공유됨 멀티스레드 프로세스는 PCB + TCB1 + TCB2 + ... 의 형태로 관리됨 🔵 컨텍스트 스위칭 컨텍스트 스위칭이란 CPU가 하나의 프로세스/스레드 실행을 중단하고, 다른 프로세스/스레드로 전환할 때의 작업 전체 이 과정에서 이전 작업의 상태(context)를 저장하고, 다음 작업의 상태를 복원하는 작업이 수행됨 컨텍스트 스위칭이 필요한 이유 멀티태스킹: 여러 작업을 동시에 실행하는 것처럼 보이게 하기 위해 시분할 시스템: 각 프로세스에 CPU 시간을 분배하기 위해 인터럽트 처리: I/O 이벤트가 발생하면 다른 프로세스로 전환 우선순위 변경: 더 중요한 프로세스를 우선 실행하기 위해 프로세스 컨텍스트 스위칭 과정 1. 현재 실행 중인 프로세스(p0)에 인터럽트 or 시스템 콜 발생 2. 운영체제가 p0의 실행 상태를 PCB0에 저장 3. 스케줄러가 다음 실행할 프로세스(p1)를 선택 4. p1의 실행 상태를 PCB1에서 복원 5. CPU 제어권을 p1에게 넘김 → p1 실행 시작 이후에도 p1이 인터럽트를 받으면 다시 p0 등으로 전환될 수 있음 컨텍스트 스위칭 시 저장되는 정보 프로그램 카운터 (PC) 레지스터 값 (R0~Rn) 스택 포인터 (SP) 프로세스 상태 메모리 관련 정보 (페이지 테이블 등) 컨텍스트 스위칭의 오버헤드 문맥 전환에는 시간과 자원이 소모됨 빈번한 스위칭은 오히려 성능 저하를 유발할 수 있음 스케줄링 알고리즘이 이를 효율적으로 조절해야 함 따라서, 운영체제는 스케줄링 알고리즘을 통해 스위칭 빈도와 시점을 최적화하려고 함

2025년 03월 26일 05:27

프론트엔드

Next.js 마이그레이션: 서버 컴포넌트(SSC)와 클라이언트 컴포넌트(CSC) 선택하기

React 프로젝트를 리팩토링 하면서 Next.js로 마이그레이션을 하게 되었고, 처음으로 서버 사이드 컴포넌트(Server-side Component, SSC) 를 사용할 기회를 갖게 되었다. 기존 React에서는 React Query를 활용하여 서버의 부담을 줄이며 데이터를 관리해왔는데, Next.js에서도 기존처럼 React Query를 사용할 수 있을지 고민하게 되었다. SSC와 CSC의 차이 Next.js의 서버 컴포넌트(SSC) 와 클라이언트 컴포넌트(CSC) 는 동작 방식이 다르다. SSC(Server-side Component): 서버에서 데이터를 가져와 미리 렌더링하여 클라이언트에 전달함 → SEO 최적화 CSC(Client-side Component): 클라이언트에서 데이터를 가져오고, 동적으로 업데이트함 → 실시간 변화 가능 이전처럼 React Query를 사용하여 데이터를 가져오려 했지만, 만약 모든 데이터를 클라이언트에서 가져온다면 Next.js의 서버 렌더링 기능을 제대로 활용하지 못하게 된다. Next.js를 사용하면서 서버의 역할을 최대한 활용해야 한다는 점에서 React Query만을 사용할 수는 없었다. 서버 부담 문제와 캐싱 처음에는 페이지를 이동할 때마다 서버가 데이터를 불러오면 부담이 커질 것이라고 생각했다. 하지만 Next.js에서는 서버에서 데이터를 가져올 때 캐싱을 통해 성능을 최적화할 수 있는 방법이 있었다. 위와 같이 revalidate: 60을 설정하면 60초 동안 같은 요청을 반복하지 않고 캐싱된 데이터를 반환한다. 덕분에 SEO를 챙기면서도 서버의 부담을 줄일 수 있다. 어떤 데이터는 SSC, 어떤 데이터는 CSC로? 처음에는 서버와 직접적인 연관이 있는 것은 SSC로, 정적인 것은 CSC로 사용해야 한다고 생각했는데, 사실은 그 반대였다. ✔️ 서버 컴포넌트(SSC)를 사용해야 하는 경우 SEO가 중요한 데이터 (검색 엔진 노출 필요) 초기 렌더링이 빠른 것이 중요한 데이터 변경이 자주 일어나지 않는 데이터 페이지 구조를 구성하는 주요 데이터 ✅ 예시 블로그 글 목록 (게시글 리스트) 블로그 글 내용 (제목, 본문) 사용자 프로필 정보 (변경이 잦지 않은 경우) 카테고리 목록 ✔️ 클라이언트 컴포넌트(CSC)를 사용해야 하는 경우 실시간으로 변화하는 데이터 사용자와의 인터랙션이 필요한 데이터 페이지 내에서 상태 변화가 필요한 데이터 ✅ 예시 좋아요 개수, 댓글 검색 기능 (검색어 입력 시마다 데이터 변경) 무한 스크롤 (React Query의 useInfiniteQuery 활용) 사용자 설정 변경 결론: SSC와 CSC의 최적 조합 초기 데이터를 서버에서 받아와 렌더링해야 하는 경우 → SSC 사용 자주 변경되거나 사용자 인터랙션이 필요한 경우 → CSC + React Query 사용 서버 부담을 줄이기 위해 revalidate 로 캐싱 활용 Next.js 마이그레이션을 하면서 서버 컴포넌트와 클라이언트 컴포넌트를 적절히 나누는 것이 성능 최적화에 매우 중요하다는 것을 깨달았다. SEO가 필요한 데이터는 서버에서 미리 받아오고, 실시간 업데이트가 필요한 데이터는 클라이언트에서 관리하는 것이 핵심이다. 이를 잘 활용하면 서버 부담을 줄이면서도 빠른 렌더링을 유지할 수 있다.

2025년 03월 18일 23:12

프론트엔드

최고의 프로젝트 구조를 찾아서....

프론트엔드 디자인 패턴: Atomic Design + FSD 적용기 프롤로그: 협업을 시작하며 처음으로 현업에서 종사하는 여러 프론트엔드 개발자들과 협업을 진행하면서 다양한 프론트엔드 디자인 패턴에 대해 깊이 고민할 기회가 생겼다. 그중에서도 가장 큰 고민은 어떻게 폴더 구조를 정리하고, 컴포넌트를 재사용할 것인가? 였다. 이 과정에서 접하게 된 대표적인 디자인 패턴이 바로 Atomic Design Pattern과 Feature-Sliced Design(FSD) 이다. 이번 글에서는 Atomic Design의 장점과 한계, 그리고 이를 보완하기 위해 FSD와 결합한 방식을 소개하려 한다. Atomic Design Pattern 적용기 Atomic Design Pattern이란? Brad Frost가 제안한 UI 설계 방식으로, 컴포넌트를 작은 단위에서 점점 확장하는 방식이다. UI를 다음 5가지 단계로 구성한다. 1. Atoms (원자) – 더 이상 쪼갤 수 없는 최소한의 UI 요소 (ex. Button, Input, Typography) 2. Molecules (분자) – Atom이 조합된 단위 (ex. SearchBar, Dropdown) 3. Organisms (유기체) – 여러 Molecule이 조합된 UI 블록 (ex. Header, Footer) 4. Templates (템플릿) – Organism을 배치한 UI 레이아웃 5. Pages (페이지) – 최종 데이터가 반영된 화면 Atomic Design Pattern 적용 시 폴더 구조 이런 구조를 사용하면 컴포넌트를 조합하는 방식이 명확해지고, 재사용성이 높아지는 장점이 있다. 하지만 실제 프로젝트에 적용하면서 몇 가지 한계를 경험했다. Atomic Design의 한계와 고민 Feature-Sliced Design(FSD)이란? FSD(Feature-Sliced Design) 은 기능(Feature) 단위로 폴더를 구성하는 방식이다. • 컴포넌트는 기능 단위로 묶는다. • 비즈니스 로직, 서비스 로직, UI 컴포넌트를 명확하게 구분한다. • 페이지별로 독립적인 폴더 구조를 유지하여 유지보수가 쉽다. Atomic Design + FSD 결합 후 디렉토리 구조 • Atomic Design은 components 폴더에서 UI 요소를 관리하는 데 적용 • FSD는 features 폴더에서 기능별로 폴더를 나누어 관리 • 공통적으로 사용하는 로직(hooks, api 등)은 shared 폴더에 배치 적용 후 개선된 점 폴더 구조가 기능 중심으로 정리되어 유지보수가 쉬워졌다. UI 컴포넌트와 비즈니스 로직을 분리할 수 있었다. 페이지 단위로 기능을 나누면서 개발 속도가 빨라졌다. 고민했던 점 Atomic Design을 어디까지 적용할 것인가? • Atom/Molecule/Organism으로 나누는 것은 좋은데, 모든 컴포넌트가 Atomic한 것은 아니다. • 어떤 기준으로 구분할지 논의가 필요했다. FSD의 Feature 컴포넌트는 어디에 넣어야 할까? • features/{feature}/components 안에 넣어야 할까? • 아니면 components/organisms로 가야 할까? • 결국 독립적인 기능일 경우 features 안에 넣고, 재사용할 UI는 components에 넣기로 결정 3. FSD 적용 후 폴더 depth를 어떻게 제한할 것인가? • 너무 깊은 구조를 만들지 않기 위해 최대 3 depth 이하로 제한 • 예외적으로 스터디 만들기 폼처럼 많은 컴포넌트가 필요한 경우 한 depth 추가 최종 정리 & 회고 Atomic Design과 FSD를 결합한 이유? • Atomic Design은 UI 컴포넌트의 재사용성을 높이는 데 효과적 • FSD는 비즈니스 로직을 기능 단위로 구분하여 유지보수를 쉽게 함 • 두 가지를 조합하면 UI와 기능을 분리하면서도 재사용성을 높일 수 있음 적용하면서 느낀 점 • Atomic만 적용하면 폴더 구조가 너무 깊어지고 관리가 어려워진다. • FSD만 적용하면 재사용성이 떨어질 수 있다. • 적절한 기준을 정하는 것이 중요하며, 프로젝트의 규모와 특성에 맞춰 유연하게 적용하는 것이 중요하다.

2025년 03월 13일 02:45

프론트엔드

React Query로 데이터 처리의 효율성 높이기

React Query를 왜 사용해야 하는가? 사실, React Query 사용 전에는 서버 담당자가 자꾸 React Query를 쓰세요 라고 권장하는 이유를 잘 이해하지 못했습니다. 난 함수 호출 잘 하고있는데? 굳이 배워야해? 라는 마인드였죠 하지만 블로그 코드를 리팩토링하고 뜯어보면서, account 정보를 불러오는 함수를 너무 많이 호출한다는 사실을 깨달았습니다. (이 블로그 왼쪽에 위치한 그 유저 정보 맞습니다.) 그러면서 그냥 유저 정보를 Recoil로 담아서 전역으로 관리할까? 라는 생각도 들었습니다. 하지만 유저 정보가 바뀌었을 때, 또 다른 곳에서 이 함수를 호출해야 한다는 문제가 생깁니다. 그리고 또 고민이 생겼습니다. 지금 계속 호출하는 게 서버에 괜찮나? 현재는 나 혼자만 사용하지만, 더 많은 사용자가 들어오게 된다면 서버에 큰 부담이 될 것이라는 생각이 들었습니다. 이렇게 고민하던 중에, React Query가 제공하는 자동 캐싱과 최신 데이터 요청 기능이 문제를 해결해줄 수 있다는 것을 알게 되었습니다. React Query란? React Query는 데이터 페칭과 상태 관리를 매우 효율적으로 처리할 수 있는 라이브러리입니다. React Query는 서버에서 데이터를 가져오는 작업을 간편하게 만들어주며, 비동기 작업을 처리하고 그 결과를 캐싱하여 성능을 최적화 합니다. 주요 특징은 다음과 같습니다. 자동 캐싱 및 재사용: 동일한 데이터 요청이 있을 경우, 이전에 받은 데이터를 재사용하여 네트워크 요청을 최소화합니다. 자동 갱신: 데이터가 변경되면, 자동으로 최신 데이터를 가져옵니다. 로딩, 오류 및 데이터 상태 관리: 로딩 상태, 오류 상태 등을 관리할 수 있는 기능을 제공하여 상태 관리가 간편해집니다. 쿼리 무효화 및 재요청: 데이터를 수정한 후, 쿼리를 무효화하여 최신 데이터를 다시 요청할 수 있습니다. 여태까지 내가 계속 해왔던 방법 - 함수 계속 호출하기 리액트 프로젝트에서 데이터를 처리할 때, useEffect 안에서 함수를 계속 호출하는 방식을 사용했습니다. 이 방식은 간단하지만, 몇 가지 단점이 존재합니다: 반복되는 코드: 데이터 요청이 여러 곳에서 반복되어 코드 중복이 발생합니다. 로딩 상태와 에러 처리: loading, error 상태를 일일이 관리해야 하므로 코드가 복잡해집니다. 성능 문제: 데이터 요청이 자주 발생하면 불필요한 네트워크 요청이 많아져 성능에 영향을 줄 수 있습니다. React Query로 바꿔보자 위 코드는 React Query의 useQuery 훅을 사용하여 유저 정보를 서버에서 가져오는 함수입니다. 이 코드는 자동 캐싱과 데이터 갱신 기능을 통해 데이터를 처리합니다. 쿼리 키와 데이터 요청 useQuery는 queryKey를 통해 요청을 구분하고, queryFn을 통해 데이터를 요청합니다. queryKey:해당 데이터 요청을 고유하게 식별 queryFn: 실제로 데이터를 요청하는 함수 캐시 관리 staleTime: 데이터가 오래된 것으로 간주되기 전에 데이터를 얼마나 유효하게 유지할지를 결정 cacheTime: 데이터가 캐시에서 삭제되기까지의 시간을 설정 에러 처리 및 로딩 상태 onError: 데이터 요청 중 오류가 발생했을 때 호출 isLoading: 요청 중 로딩 상태 error: 요청 실패 시의 에러 정보 refetch refetch: 데이터를 수동으로 다시 요청할 수 있는 함수 그래서 React Query가 뭐가 다른데? 이 질문이 나온 이유는, React Query를 사용해도 새로고침을 하면 다시 fetch가 된다는 점에서 의문이 발생했기 때문입니다. 새로고침만 해도 쿼리가 다시 실행되는데 그러면 내가 모든 컴포넌트에 account 함수 호출하는거랑 뭐가 달라? 제가 의문이 생겨서 chatGPT에게 실제로 물어봤던 내용입니다. 기존 방식에서는 페이지를 새로고침할 때마다 데이터를 새로 요청하게 되며, 이를 계속 함수 호출이라고 표현할 수 있습니다. 이 방식은 사용자가 새로고침을 할 때마다 매번 API 요청을 보내고, 그에 따른 로딩 상태가 표시되기 때문에 불편할 수 있습니다. 또, 같은 데이터를 반복적으로 요청하게 되어 성능에도 영향을 미칩니다. 반면, React Query는 자동 캐싱 기능을 제공하여, 한 번 데이터를 요청하고 나면 캐시에서 데이터를 가져옵니다. 이후에는 서버에 다시 요청을 보내지 않고, 이미 받은 데이터를 사용할 수 있기 때문에 불필요한 요청을 줄일 수 있습니다. 그리고 데이터가 수정되거나 갱신될 때는 invalidateQueries나 refetchQueries를 통해 필요한 시점에만 다시 요청을 보내어 최신 상태로 동기화할 수 있습니다. 차이점 1: 데이터 요청 및 캐시 관리 • 기존 방식: 페이지 새로 고침 시마다 데이터를 새로 요청. • React Query: 데이터가 캐시되어, 동일한 데이터 요청 시 네트워크 요청을 최소화하고, 자동으로 최신 상태를 유지. 차이점 2: 로딩 상태 관리 • 기존 방식: 페이지 새로 고침 시마다 로딩 상태가 반복적으로 표시. • React Query: 이미 캐시된 데이터가 있으면 로딩 없이 바로 데이터를 표시하고, 필요 시 백그라운드에서 갱신. React Query의 캐싱과 탭 전환 여기서 중요한 점은 다른 탭으로 넘어갔다가 다시 돌아올 때도 React Query는 이미 캐시된 데이터를 사용한다는 점입니다. 기존 방식에서는 페이지를 새로고침할 때마다 데이터를 요청해야 했지만, React Query는 데이터가 캐시되기 때문에 같은 데이터를 다시 요청하지 않고, 캐시된 데이터를 사용합니다. 예를 들어, useQuery로 한 번 데이터를 요청하고 나면, 해당 데이터는 캐시에 저장됩니다. 다른 페이지나 탭으로 이동하더라도, 같은 데이터를 다시 요청하지 않고 캐시된 데이터를 즉시 사용할 수 있습니다. 만약 데이터가 변경되었다면, React Query는 invalidateQueries나 refetchQueries를 통해 데이터를 새로 요청하여 자동으로 최신 데이터를 반영합니다. 이렇게 React Query는 데이터 요청을 효율적으로 관리하며, 불필요한 요청을 줄이고, 사용자 경험을 개선할 수 있습니다. 🚨문제: 데이터 변경 요청 후 캐시된 데이터 유지 문제는, userData 수정 요청을 보내고 데이터를 변경한 후에도 캐시된 데이터가 계속 유지된다는 점입니다. 예를 들어, 사용자가 자신의 정보를 수정하는 API 요청을 보낸 후, 데이터베이스에는 수정된 내용이 반영되었지만, React Query의 캐시에는 여전히 이전 데이터가 남아 있을 수 있습니다. 이 경우, 수정된 데이터가 즉시 반영되지 않고, 여전히 오래된 캐시 데이터를 사용하게 됩니다. 문제 발생 이유 • 캐시와 서버 데이터 간 불일치: useQuery로 데이터를 요청한 후 React Query는 캐시된 데이터를 사용하고, 수정된 데이터는 서버에서 갱신되었지만, 캐시에는 여전히 이전 데이터가 저장되어 있을 수 있습니다. • 자동 갱신 미흡: invalidateQueries나 refetchQueries를 사용하지 않으면, 수정된 데이터를 즉시 다시 요청하지 않기 때문에 캐시된 데이터가 계속 사용됩니다. refetch( ) 사용으로 해결하기 이 문제를 해결하기 위해서는 useQuery로 데이터를 요청하고, refetch( )를 사용하여 수정된 데이터를 강제로 다시 가져오는 방법이 필요합니다. refetch( )는 useQuery로 가져온 데이터를 다시 요청하는 함수로, 데이터 수정 후 필요한 시점에만 다시 요청을 보내어 최신 상태로 동기화할 수 있습니다. 예를 들어, userData를 수정한 후 refetch( )를 사용하여 해당 데이터를 다시 가져올 수 있었습니다: 결론 React Query는 서버 부하를 줄이고, 데이터 요청을 효율적으로 관리하는 데 중요한 도구입니다. 자동 캐싱과 데이터 갱신 기능을 통해 불필요한 네트워크 요청을 줄이고, 서버에 가는 요청을 최소화할 수 있습니다. 특히 refetch( )를 사용하여 데이터 수정 후 즉시 최신 데이터를 요청할 수 있어, 서버와 클라이언트 간의 데이터 동기화를 원활하게 할 수 있습니다. 기존 방식에서 발생할 수 있는 중복된 요청과 성능 문제를 해결하면서, React Query는 데이터 관리의 효율성을 높이고 서버 성능 최적화에도 기여할 수 있습니다. 따라서, React Query는 단순한 데이터 요청 라이브러리를 넘어서, 서버 자원을 효율적으로 관리하고 성능을 최적화하는 데 필요한 필수 도구라고 할 수 있습니다.

2025년 01월 20일 09:40