일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 나만무
- 리액트
- 모션비트
- TiL
- Vue.js
- HTML
- 오블완
- 백준
- JavaScript
- 크래프톤정글
- defee
- 자바스크립트
- 코드트리
- 티스토리챌린지
- 자바
- userprog
- 소켓
- Flutter
- corou
- 알고리즘
- CSS
- 핀토스
- 큐
- 사이드프로젝트
- pintos
- 크래프톤 정글
- 4기
- 시스템콜
- 스택
- Java
- Today
- Total
미새문지
24.07.11 day23 뮤텍스와 세마포어, 데드락(Deadlock), 프로그램 컴파일 본문
뮤텍스와 세마포어
뮤텍스와 세마포어의 차이는 무엇인가요?
뮤텍스(Mutex)와 세마포어(Semaphore)는 둘 다 동기화 기법으로, 멀티스레드 환경에서 공유 자원의 접근을 제어하는데 사용된다.
차이점
뮤텍스(Mutex)
- 정의: 뮤텍스는 하나의 스레드만이 특정 자원에 접근할 수 있도록 보장하는 동기화 기법이다.
- 소유권: 뮤텍스는 소유권 개념이 있다. 즉, 뮤텍스를 소유한 스레드만이 뮤텍스를 해제할 수 있다.
- 사용 사례: 단일 자원을 보호해야 하는 경우에 주로 사용된다.
- 예를 들어, 하나의 스레드만이 특정 데이터 구조에 접근하고 수정할 수 있도록 할 때 사용된다.
- 기본 동작: 뮤텍스는 잠금을 걸고, 잠금을 해제하는 방식으로 동작한다. 한 스레드가 뮤텍스를 잠그면, 다른 스레드는 그 뮤텍스가 해제될 때까지 대기해야 한다.
세마포어(Semaphore)
- 정의: 세마포어는 정수 값을 가지는 동기화 기법으로, 특정 자원에 접근할 수 있는 스레드의 수를 제어한다.
- 소유권: 세마포어에는 소유권 개념이 없다. 즉, 세마포어를 기다리는 스레드와 해제하는 스레드가 다를 수 있다.
- 사용 사례: 여러 개의 스레드가 접근할 수 있는 자원의 수를 제한해야 할 때 사용된다.
- 예를 들어, 제한된 수의 데이터베이스 연결을 관리할 때 사용된다.
- 기본 동작: 세마포어는 스레드의 상태에 따라 값을 증가시키거나 감소시킬 수 있으며 값이 1이상일 때는 접근이 가능하고 값이 0일 때는 접근이 불가능하다.
- 타입: 세마포어에는 두 가지 타입이 있다.
- 카운팅 세마포어: 세마포어의 값이 0 이상일 수 있으며, 자원의 개수를 나타낸다.
- 바이너리 세마포어: 값이 0 또는 1만을 가지며, 이 경우 뮤텍스와 유사하게 동작한다.
즉, 뮤텍스는 단일 스레드만이 자원에 접근할 수 있도록 보장하며, 소유권 개념이 있다. 세마포어는 자원에 접근할 수 있는 스레드의 수를 제어하며, 소유권 개념이 없다.
lock을 얻기 위해 대기하는 프로세스들은 Spin Lock 기법을 사용할 수 있습니다. 이 방법의 장단점은 무엇인가요? 단점을 해결할 방법은 없을까요?
스핀 락(Spin Lock)은 락을 얻기 위해 대기하는 동안 스레드가 CPU 사이클을 소모하며 계속해서 락을 시도하는 기법이다.
스핀 락의 장점
- 빠른 응답성: 스핀 락은 락을 얻을 때까지 계속해서 시도하므로, 락이 짧은 시간 내에 해제될 경우 빠르게 락을 얻을 수 있다.
- 문맥 전환 오버헤드 없음: 스핀 락은 스레드를 블록하지 않기 때문에, 컨텍스트 스위칭으로 인한 오버헤드가 없습니다. 이는 락이 짧은 시간 내에 해제될 경우 성능에 유리하다.
- 간단한 구현: 스핀 락은 비교적 단순하게 구현할 수 있으며, 운영 체제의 지원 없이도 사용자 수준에서 사용할 수 있다.
스핀 락의 단점
- CPU 자원 낭비: 스핀 락은 락을 얻기 위해 계속해서 루프를 돌기 때문에, CPU 자원을 낭비하게 된다. 특히, 락이 오래 걸릴 경우 심각한 성능 저하를 초래할 수 있다.
- 우선 순위 역전 문제: 스핀 락은 우선 순위가 낮은 스레드가 락을 소유하고 있는 경우, 우선 순위가 높은 스레드도 락을 얻기 위해 계속해서 대기하게 되어 우선 순위 역전 문제가 발생할 수 있다.
- 비효율성: 락이 긴 시간 동안 유지되는 경우, 스핀 락은 비효율적이게 되는데, 이 경우 스레드를 블록하는 방법이 더 적합할 수 있다.
단점을 해결할 방법
- 하이브리드 락: 스핀 락과 블로킹 락의 장점을 결합한 하이브리드 락을 사용한다.
- 스레드가 일정 시간 동안 스핀을 시도하고, 그 후에도 락을 얻지 못하면 블록 상태로 전환되는데, 이는 짧은 대기 시간에는 스핀 락의 이점을 활용하고, 긴 대기 시간에는 CPU 자원을 절약할 수 있게 된다.
- 적응형 스핀 락: 락의 사용 패턴에 따라 스핀 시간의 길이를 동적으로 조정하는 방법이다.
- 락이 짧은 시간 내에 해제될 가능성이 높을 경우, 스핀 시간을 길게 하고, 그렇지 않으면 스핀 시간을 줄인다.
- 우선 순위 상속: 우선 순위 역전 문제를 해결하기 위해 우선 순위 상속 기법을 사용한다.
- 락을 소유한 스레드가 높은 우선 순위의 스레드로부터 락을 요청받으면, 락을 해제할 때까지 우선 순위가 일시적으로 상속된다.
- CPU 휴식 명령어 사용: 스핀 락 루프 내에서 PAUSE 명령어와 같은 CPU 휴식 명령어를 사용하여 CPU 자원의 낭비를 줄인다.
- 이는 CPU가 다른 작업을 수행할 수 있는 기회를 제공하여 자원 낭비를 줄이는 효과가 있다.
이러한 방법들을 통해 스핀 락의 단점을 완화하고, 성능과 효율성을 높일 수 있다.
뮤텍스와 세마포어 모두 커널이 관리하기 때문에, Lock을 얻고 방출하는 과정에서 시스템 콜을 호출해야 합니다. 이 방법의 장단점이 있을까요? 단점을 해결할 수 있는 방법은 없을까요?
시스템 콜을 통한 락 관리의 장점
- 안정성: 커널에서 관리되기 때문에, 동기화 메커니즘이 안정적이고 신뢰성이 있다.
- 공정성: 커널은 세마포어 등의 기법을 통해 공정한 자원 분배를 보장할 수 있다.
- 우선 순위 관리: 커널 수준에서 우선 순위 상속 기법같은 우선 순위 역전 문제를 해결하기 위한 기법들을 효과적으로 적용할 수 있다.
- 프로세스 간 동기화: 커널은 프로세스 간 동기화를 지원하기 때문에, 같은 시스템 내의 여러 프로세스가 동일한 락을 사용할 수 있다.
시스템 콜을 통한 락 관리의 단점
- 오버헤드: 시스템 콜은 사용자 모드에서 커널 모드로 전환이 반복되며, 이로 인해 상당한 오버헤드가 발생할 수 있다.
- 컨텍스트 스위칭: 락을 얻기 위해 블록될 경우, 컨텍스트 스위칭이 발생하여 추가적인 성능 저하가 발생할 수 있다.
- 복잡성: 커널 수준에서 동기화를 관리하기 때문에, 구현과 유지보수가 복잡할 수 있다.
단점을 해결할 수 있는 방법
- 스핀 락: 간단한 경우에는 스핀 락을 사용하여 사용자 모드에서 락을 관리할 수 있다. 이는 시스템 콜의 오버헤드를 피할 수 있지만, 스핀 락의 단점을 해결해야 한다.
- 배리어 락: 사용자 모드에서 구현된 배리어를 사용하여 특정 조건이 만족될 때까지 대기하는 방식으로, 시스템 콜의 빈도를 줄일 수 있습니다
- Futex(Fast Userspace Mutexes): 리눅스에서 제공하며 사용자 모드에서 빠르게 락을 획득하고 해제할 수 있도록 설계된 메커니즘이다.
- Futex는 락이 경쟁상태가 없을 때는 사용자 모드에서 처리하고, 있을 때만 커널 모드로 전환하여 처리하는데, 이는 시스템 콜의 오버헤드를 크게 줄여준다.
- 락 분할(Lock Splitting): 락을 세분화하여 여러 락으로 나누어 경합을 줄인다.
- 예를 들어, 데이터 구조의 각 부분에 대해 별도의 락을 사용하는 방식이 있다.
- 락 스트리핑(Lock Stripping): 해시 테이블 등의 구조에서, 각 버킷에 별도의 락을 적용하여 경합을 줄이는 방식이다.
데드락(Deadlock)
Deadlock이 발동하는 4가지 조건에 대해 설명해주세요.
데드락은 여러 프로세스가 서로 자원을 기다리며 무한히 대기 상태에 빠지는 상황을 의미하며, 데드락이 발생하기 위해서는 네 가지 필수 조건이 필요하다.
1. 상호 배제(Mutual Exclusion)
- 설명: 자원은 동시에 하나의 프로세스만 사용할 수 있다. 즉, 자원이 이미 다른 프로세스에 의해 사용 중인 경우, 다른 프로세스는 그 자원이 해제될 때까지 기다려야 한다.
- 예시: 프린터, 파일 쓰기 작업, 데이터베이스 레코드 등은 동시에 여러 프로세스가 사용할 수 없다.
2. 점유 대기(Hold and Wait)
- 설명: 이미 자원을 점유하고 있는 프로세스가 다른 자원을 추가로 요청할 때, 해당 자원이 해제될 때까지 그 자원을 기다린다. 즉, 자원을 점유한 상태에서 추가 자원을 요청하는 상황이 발생한다.
- 예시: 프로세스 A가 자원 R1을 점유하고 있고, 자원 R2를 요청하지만 R2는 다른 프로세스 B가 점유하고 있는 경우, 프로세스 A는 R2가 해제될 때까지 기다리게 된다.
3. 비선점(No Preemption)
- 설명: 다른 프로세스가 점유하고 있는 자원을 강제로 빼앗을 수 없다. 자원은 자원을 점유하고 있는 프로세스가 자발적으로 해제할 때까지 기다려야 한다.
- 예시: 프로세스가 프린터를 점유하고 있는 동안, 다른 프로세스는 프린터를 강제로 가져올 수 없고, 프린터가 자발적으로 반환될 때까지 기다려야 한다.
4. 순환 대기(Circular Wait)
- 설명: 데드락이 발생하는 프로세스들이 자원 대기 형태로 원형을 이루는 경우를 말한다. 즉, 프로세스 P1이 자원 R2를 기다리고 있고, 프로세스 P2가 자원 R3를 기다리고 있으며, 마지막으로 프로세스 Pn이 자원 R1을 기다리는 형태로 순환 대기를 이루는 경우이다.
- 예시: 프로세스 A가 자원 R1을 점유하고 자원 R2를 기다리고, 프로세스 B가 자원 R2를 점유하고 자원 R3를 기다리며, 프로세스 C가 자원 R3을 점유하고 자원 R1을 기다리는 상황이다. 이렇게 되면 원형 대기 상태가 되어 데드락이 발생하게 된다.
이 중 하나가 빠진다고 가정하면 왜 Deadlock이 발생하지 않을까요?
데드락은 "상호 배제, 점유 대기, 비선점, 순환 대기" 이 네 가지 조건이 동시에 만족될 때 데드락이 발생하는데, 이러한 이유로 이 조건 중 하나라도 만족하지 않으면 데드락이 발생하지 않는지를 알 수 있다.
- 상호 배제 조건이 없으면 자원을 여러 프로세스가 동시에 사용할 수 있으므로, 자원 사용 때문에 프로세스가 대기할 필요가 없어진다.
- 점유 대기 조건이 없으면 프로세스가 자원을 점유한 상태에서 다른 자원을 요청할 수 없으므로, 자원을 점유한 프로세스가 대기 상태에 들어가지 않는다.
- 비선점 조건이 없으면, 자원을 요청하는 프로세스가 필요한 자원을 점유하지 못하는 경우 다른 프로세스가 자원을 강제로 회수할 수 있으므로, 프로세스가 자원을 기다리며 교착 상태에 빠질 가능성이 줄어든다.
- 순환 대기 조건이 없으면, 대기 상태에 있는 프로세스들이 서로 자원을 순환적으로 기다리는 상황이 발생하지 않으므로, 모든 프로세스가 자원을 얻기 위한 대기열을 형성하지 않는다.
Deadlock 예방은 어떻게 하나요?
데드락은 네 가지 조건 중 하나라도 만족되지 않으면 발생하지 않는다. 따라서 하나 이상이 충족되지 않도록 예방해야 한다.
상호 배제 조건 방지
- 가능한 경우, 자원을 공유하여 여러 프로세스가 동시에 사용할 수 있도록 한다.
- 예를 들어, 읽기 전용 파일은 여러 프로세스가 동시에 읽을 수 있다.
- 그러나, 대부분의 경우 상호 배제는 필수적인 자원의 속성이므로 이 방법은 제한적일 수 있다.
점유 및 대기 조건 방지
- 프로세스가 시작될 때 필요한 모든 자원을 한 번에 할당받도록 한다. 이렇게 하면 프로세스가 실행 중에 추가 자원을 요청하지 않게 된다.
- 혹은 자원을 요청하기 전에 점유하고 있는 자원을 모두 해제한 후 다시 요청하게 한다.
비선점 조건 방지
- 자원을 점유하고 있는 프로세스가 추가 자원을 요청할 때, 요청한 자원을 즉시 할당하지 못하면 점유하고 있는 자원을 모두 해제하게 하며, 이후 필요한 모든 자원을 다시 요청하게 한다.
- CPU 스케줄링의 경우, 자원을 강제로 회수하는 방식으로 데드락을 예방할 수 있다.
순환 대기 조건 방지
- 모든 자원 유형에 일관된 순서를 부여하고, 프로세스가 자원을 요청할 때 정해진 순서대로 요청하게 하여, 자원 순서를 강제하는 방법으로 순환 대기를 방지할 수 있다.
- 예를 들어, 자원 A, B, C가 있을 때 항상 A를 먼저 요청하고, 그 다음에 B, 그 다음에 C를 요청하도록 한다.
Wait Free와 Lock Free를 비교해 주세요.
Wait-Free와 Lock-Free는 둘 다 병렬 프로그래밍에서 동기화 메커니즘으로 사용되며, 동시성을 관리하면서 데드락, 기아 현상 등의 문제를 방지하기 위해 고안되었다.
Lock Free
- 여러 스레드가 공유 자원에 접근할 수 있으며, 어떤 스레드도 다른 스레드로 인해 무한히 대기하게 되지 않는 알고리즘이다.
- 하나의 스레드가 실패하거나 지연되더라도, 다른 스레드는 계속해서 진행될 수 있습니다.
Wait Free
- Lock Free의 한 형태로, 모든 스레드가 제한된 수의 단계 안에 작업을 완료할 수 있음을 보장하는 알고리즘이다.
- 이는 시스템 전반의 처리량을 보장하며, 모든 스레드가 계속해서 진행될 수 있도록 한다.
특성 | Wait Free | Lock Free |
진행 보장 | 모든 스레드가 제한된 시간 내에 작업을 완료함 | 최소 한 스레드가 계속해서 작업을 진행함 |
복잡성 | 구현이 매우 복잡하고 성능이 낮을 수 있음 | 구현이 비교적 간단하고 성능이 좋을 수 있음 |
기아 방지 | 모든 스레드가 기아 상태에 빠지지 않음 | 일부 스레드가 기아 상태에 빠질 수 있음 |
사용 사례 | 실시간 시스템, 극단적인 고성능 요구 시스템 | 일반적인 병렬 프로그램, 성능이 중요한 응용 프로그램 |
데드락 방지 | 데드락을 방지함 | 데드락을 방지함 |
프로그램 컴파일
링커와 로더의 차이에 대해 설명해주세요.
링커(Linker)
링커는 프로그램이 여러 개의 모듈로 나뉘어 개발될 때, 이러한 모듈들을 하나로 결합하여 하나의 실행 가능한 파일로 만드는 도구이며, 주로 컴파일된 코드의 단계에서 사용된다.
- 심볼 해결: 각각의 모듈에서 사용된 변수나 함수의 심볼을 실제 메모리 주소나 다른 모듈 내의 심볼로 매핑한다.
- 주소 결정: 각각의 모듈이 메모리에 로드될 때, 모듈 내부의 상대적인 주소들을 절대적인 주소로 변환한다.
- 라이브러리 링킹: 다른 라이브러리에 정의된 함수들을 필요할 때 로드하고 연결한다.
- 중복 코드 제거: 사용되지 않는 코드를 제거하여 최적화한다.
로더 (Loader)
로더는 컴퓨터에서 실행 가능한 프로그램을 메모리에 로드하고 실행할 수 있도록 도와주는 프로그램이며, 주로 운영 체제의 일부로 작동한다.
- 로드: 실행 파일을 디스크에서 메모리로 읽어들인다.
- 링크: 실행 파일 내의 다른 모듈이나 라이브러리들과 연결하여 하나의 실행 가능한 프로그램을 만든다.
- 주소 변환: 프로그램이 메모리에 로드될 때, 각 코드와 데이터의 물리적 메모리 주소를 결정한다.
- 실행: 메모리에 로드된 프로그램을 실제로 실행시킨다.
차이점
로더는 컴퓨터 메모리에 프로그램을 로드하고 실행할 수 있도록 돕는 반면, 링커는 여러 모듈로 나눠진 프로그램을 하나로 합치고, 심볼 해결과 주소 결정 등을 통해 실행 가능한 파일을 생성한다.
따라서 로더와 링커는 프로그램 개발 및 실행 과정에서 서로 보완적인 역할을 수행한다.
컴파일 언어와 인터프리터 언어의 차이에 대해 설명해주세요.
컴파일 언어와 인터프리터 언어는 프로그래밍 언어의 실행 방식에 따라 구분되며, 각 방식은 프로그램의 실행 속도, 디버깅 용이성, 이식성 등에 영향을 미친다.
컴파일 언어 (Compiled Language)
컴파일 언어는 소스 코드를 한 번에 기계어로 번역하는 과정을 거쳐 실행 파일을 생성하는 방식이며, 이 과정은 컴파일러라는 도구에 의해 수행된다.
특징
- 컴파일 과정:
- 소스 코드를 컴파일러가 한 번에 번역하여 실행 파일(바이너리 파일)을 생성한다.
- 생성된 실행 파일은 운영 체제에서 직접 실행될 수 있다.
- 실행 속도:
- 실행 파일이 이미 기계어로 번역되어 있기 때문에 실행 속도가 빠르다.
- 런타임 시 추가적인 번역이 필요하지 않다.
- 디버깅 및 개발 과정:
- 컴파일 과정에서 발생하는 오류를 미리 확인할 수 있다.
- 디버깅이 어려울 수 있으며, 코드 수정 후에는 다시 컴파일해야 한다.
- 이식성:
- 실행 파일은 특정 운영 체제와 하드웨어에 종속될 수 있다.
- 소스 코드를 다른 플랫폼에서 실행하려면 해당 플랫폼에 맞게 다시 컴파일해야 한다.
인터프리터 언어 (Interpreted Language)
인터프리터 언어는 소스 코드를 한 줄씩 해석하고 즉시 실행하는 방식을 사용하며, 이 과정은 인터프리터라는 도구에 의해 수행된다.
특징:
- 실행 과정:
- 소스 코드를 인터프리터가 한 줄씩 읽고, 해석하고, 실행한다.
- 별도의 실행 파일을 생성하지 않는다.
- 실행 속도:
- 소스 코드를 실행할 때마다 해석해야 하기 때문에 실행 속도가 비교적 느리다.
- 각 명령어를 즉시 해석하므로 런타임 오버헤드가 발생한다.
- 디버깅 및 개발 과정:
- 코드 수정 후 바로 실행할 수 있어 디버깅과 개발이 용이하다.
- 인터프리터는 종종 풍부한 디버깅 기능을 제공한다.
- 이식성:
- 소스 코드 자체가 이식성이 높아, 인터프리터만 있으면 다양한 플랫폼에서 실행할 수 있다.
- 플랫폼에 맞는 인터프리터가 필요하다.
컴파일 언어는 한 번의 컴파일 과정으로 빠른 실행 속도를 제공하는 반면, 인터프리터 언어는 코드의 실행과 수정이 용이하지만 실행 속도가 느릴 수 있다.
본인이 사용하는 언어는 어떤 식으로 컴파일 및 실행되는지 설명해주세요.
자바스크립트는 주로 인터프리터 언어로 알려져 있지만, 현대의 자바스크립트 엔진은 Just-In-Time(JIT) 컴파일러를 사용하여 실행 성능을 크게 향상시킨다.
자바스크립트의 컴파일 및 실행 과정
1. 소스 코드 로드
- 자바스크립트 소스 코드는 HTML 문서 내의 <script> 태그에서 로드되거나, 외부 자바스크립트 파일로부터 로드된다.
- 브라우저나 Node.js와 같은 JavaScript 환경은 이 소스 코드를 받아들인다.
2. 파싱(Parsing)
- 자바스크립트 엔진은 소스 코드를 분석하는데, 이 과정은 두 단계로 나뉜다.
- 어휘 분석(Lexical Analysis): 소스 코드를 토큰(token)이라는 작은 의미 단위로 분할한다.
- 구문 분석(Syntax Analysis): 토큰을 사용하여 추상 구문 트리(Abstract Syntax Tree, AST)를 생성합니다. AST는 코드의 구조를 나타내며, 컴파일 및 최적화를 위해 사용된다.
3. 컴파일(Compilation)
- 현대 자바스크립트는 Just-In-Time (JIT) 컴파일을 사용하여 코드를 컴파일하며, 이 과정은 크게 두 가지 단계로 나눌 수 있다.
- 바이트코드 생성(Bytecode Generation): AST를 바탕으로 중간 표현인 바이트코드를 생성한다. 바이트코드는 기계어보다는 고수준이며, 인터프리터에 의해 실행된다.
- 최적화 및 기계어 코드 생성(Optimization and Machine Code Generation): JIT 컴파일러는 실행 중인 코드를 분석하여 자주 실행되는 부분을 최적화하고, 기계어 코드로 변환한다. 이를 통해 실행 속도를 크게 향상시킬 수 있다.
4. 실행(Execution)
컴파일된 바이트코드와 기계어 코드는 자바스크립트 엔진에 의해 실행되는데, 자바스크립트 엔진은 인터프리터와 JIT 컴파일러를 조합하여 코드를 효율적으로 실행한다.
- 인터프리터: 초기에는 바이트코드를 인터프리터가 실행한다. 인터프리터는 한 줄씩 바이트코드를 해석하고 실행한다.
- JIT 컴파일러: 자주 실행되는 코드를 감지해서 최적화된 기계어 코드로 변환하는데, 이렇게 최적화된 코드는 인터프리터보다 빠르게 실행된다.
주요 JavaScript 엔진
V8(Google Chrome, Node.js)
- V8 엔진은 Google Chrome과 Node.js에서 사용되는 자바스크립트엔진이다.
- V8은 고성능을 위해 JIT 컴파일러와 여러 가지 최적화 기술을 사용한다.
결론
자바스크립트는 현대적인 엔진을 통해 소스 코드를 구문 분석하고, JIT 컴파일러를 통해 실행 시간에 코드를 컴파일하여 최적화된 기계어 코드로 변환한다.
이 과정 덕분에 JavaScript는 인터프리터 언어의 유연성을 유지하면서도 컴파일 언어의 성능을 어느 정도 구현할 수 있게 된다.
'개발 TIL' 카테고리의 다른 글
24.07.15 day25 IPC, CSR과 SSR (0) | 2024.07.15 |
---|---|
24.07.12 day24 남추문 스터디 회고 (0) | 2024.07.12 |
24.07.10 day22 스케줄링 알고리즘 학습 (0) | 2024.07.10 |
24.07.09 day21 html과 Xhtml의 차이점, Promise와 async/await의 차이점 (0) | 2024.07.09 |
24.07.08 day20 Flexbox vs Grid, 포트폴리오 배경화면 (1) | 2024.07.08 |