언어의 환상: 지시어(directive)의 정체

I
Inkyu Oh

Front-End2025.11.26

DEV Community 블로그 포스트 번역



서문 & 소개 — 이 글이 존재하는 이유

이 글은 반박이 아닙니다. Tanner Linsley의 훌륭한 "Directives and the Platform Boundary" 글에서 영감을 받은 성찰입니다. 그 글을 정말 재밌게 읽었고 많은 부분에 동의합니다 — 특히 소유권, 출처, 명시적 API의 가치에 관한 부분들이요. 제가 추가하고 싶은 것은 약간 다른 관점입니다: 지시어 기반 도구를 만들면서 얻은 경험과, JavaScript에 'use client''use server'가 있기 훨씬 전의 프로그래밍 역사에서 매우 유사한 시대를 겪었던 경험에서 나온 관점입니다.
지난 1년간 수많은 개발자들이 JavaScript의 지시어를를 마치 실제 언어 기능인 것처럼 다루는 것을 봤습니다 — JavaScript 스펙이나 런타임 환경에 내장된 무언가 말이죠. 공정하게 말하자면, 얼핏 보기에는 정말 그렇게 보입니다. 파일 맨 위의 문자열이 마법처럼 코드의 동작을 바꾸는 것처럼 보이니까요.
React Server Components와 Next.js 같은 프레임워크의 진화를 따라오지 못한 사람을 위해 설명하자면: 현대의 React 앱은 이제 'use client''use server' 같은 파일 수준의 지시어를 사용해서 코드가 어디서 실행될지를 제어합니다. 파일 맨 위의 단순한 문자열 리터럴처럼 보이지만, 컴포넌트가 서버에서 실행될지 클라이언트에서 실행될지를 결정하고 — 번들링, 렌더링, 데이터 흐름을 형성합니다. 혼동이 시작되는 지점이 바로 여기입니다.
하지만 여기 이상한점이 있습니다:
지시어(directive)는 언어 기능처럼 보입니다. 지시어는 언어 기능처럼 느껴집니다.그런데 지시어는 언어 기능이 아닙니다.
지시어는 도구 수준의 신호입니다 — 번들러, 컴파일러, 빌드 파이프라인에 의해 소비됩니다.
이런 오해는 새로운 현상이 아닙니다. 우리는 이전에 이런 상황을 겪었습니다.

개인적인 회상

C++ 코드베이스에서 처음 #pragma once를 본 젊은 개발자로서 저는 이렇게 생각했습니다: "와, 언어가 이걸 위한 키워드를 가지고 있네! JavaScript는 왜 없지?"
그런데... 그건 언어 키워드가 아니었습니다. 그건 컴파일러 명령어였습니다. 지시어였습니다. 전처리기에 대한 힌트였지, 언어 자체에 대한 것이 아니었습니다.
'use strict'가 처음에는 "JavaScript 문법"이 아니었던 것처럼, 'use client'도 오늘날 "JavaScript 문법"이 아닙니다.
이 글은 그 평행선을 탐구합니다. C/C++ 전처리기 시대를 이해하는 것이 현대 JS 지시어를 이해하는 놀랍도록 강력한 방법이기 때문입니다 — 왜 사람들이 혼동하는지, 그리고 과거에서 배워 더 잘 가르칠 수 있는 것이 무엇인지를 말이죠.

지시어가 언어 기능처럼 느껴지는 이유

새로운 사람에게 다음 코드를 보여준다면:
'use client';

export function Button() {
return <button>Click</button>;
}
그리고 'use client'가 무엇인지 묻는다면, 대부분은 자신감 있게 답할 것입니다:
"JavaScript의 일부입니다."
이건 무지가 아닙니다. 이건 패턴 인식입니다.
프로그래밍 인생 전체에 걸쳐, 파일 수준의 "마법 같은 명령문"은 거의 항상 도구 의미론이 아닌 언어 의미론이었습니다:
  • "use strict"
  • "use asm"
문법처럼 보입니다. 파일 맨 위에 있습니다. 동작을 바꿉니다.
그래서 우리의 뇌는 이를 언어로 분류합니다.
TanStack 글이 이 부분을 잘 포착했을 때:
"파일 맨 위의 지시어는 권위 있어 보입니다. 프레임워크 힌트가 아닌 언어 수준의 기능같은 인상을 줍니다."
이것이 혼동의 핵심입니다.

숨겨진 메커니즘이 더 악화시킵니다

지시어가 경계를 흐리는 이유는 이에 반응하는 코드가:
  • 보이지 않습니다
  • 로컬이 아닙니다
  • import를 통해 추적할 수 없습니다
만약 제가 이렇게 쓴다면:
import { client } from 'somewhere';
→ import를 따라갈 수 있습니다. 누가 소유하는지 봅니다. 버전을 관리할 수 있습니다. 어떤 문서를 검색해야 하는지 압니다.
하지만 지시어의 경우:
  • import가 없습니다
  • 네임스페이스가 없습니다
  • 소유권 참조가 없습니다
  • 검사할 호출 지점이 없습니다
동작이 "어디서도 오지 않습니다".
정확히 이것이 언어처럼 느껴지는 이유입니다.

빌드 도구를 추가하면... 환상이 굳어집니다

JavaScript 키워드와 달리, 지시어는 그 자체로는 아무것도 하지 않습니다.
런타임 엔진은 'use clinet'을 잘못 입력해도 에러를 던지지 않습니다. 번들러나 변환 플러그인이 이를 어떻게 할지 결정합니다. 어떤 것은 무시하고, 어떤 것은 에러를 내고, 어떤 것은 문자열 리터럴로 취급합니다.
이것이 두 번째 주요 혼동으로 이어집니다:
"번들러가 필요하다면, 언어의 일부가 아니라 생태계 플랫폼의 일부 아닌가요?"
개발자의 관점에서, 명시적 import 없이 코드가 동작을 바꾼다면, 마음은 기본적으로:
✅ 언어 기능 ❌ 라이브러리 기능
으로 기울어집니다.
그리고 그것이 함정입니다.
왜냐하면 우리가 오늘날 "플랫폼"이라고 부르는 것은 실제로는 도구의 스택이기 때문입니다 — 일관된 언어 표면이 아닙니다. C/C++ 시대와 정확히 같습니다.

돌아보기: C/C++ 매크로와 전처리기

현대 JavaScript 빌드 파이프라인이 나오기 훨씬 전에, C와 C++ 세계는 매우 유사한 혼동을 겪었습니다. 그리고 그것은 모두 컴파일 전에 있던 레이어에서 시작되었습니다: 전처리기.
오늘날의 지시어를 이해하려면, 우리는 그 시대의 세 가지 기둥을 간단히 다시 살펴봐야 합니다 — 오늘날 우리가 보는 것과 놀랍도록 잘 매핑되기 때문입니다.

C 전처리기는 언어가 아니었습니다 — 하지만 그렇게 느껴졌습니다

다음을 생각해 봅시다:
#define PI 3.14
이를 처음 만나는 많은 초보자들은 가정합니다: "아, C는 상수를 정의하기 위한 특별한 언어 키워드를 가지고 있네."
그런데... #define은 C 언어 문법의 일부가 아닙니다. 컴파일러 전에 실행되는 별도의 도구에 대한 명령어입니다.
텍스트 치환을 수행합니다 — 타입 체크가 아니라, 문법 분석이 아니라, 의미론적 검증이 아닙니다.
그런데도, .c/.h 파일 안에 있었고 "코드"처럼 보였기 때문에, 수 세대의 개발자들이 이를 언어로 인식했습니다.
익숙한가요?
JS의 지시어도 같은 방식으로 동작합니다:
'use server';
JavaScript 엔진은 그것이 무엇을 의미하는지 모릅니다. 번들러가 실행 전에 다시 쓸 것을 결정합니다.
둘 다 언어 전 단계로, 언어인 척합니다.

pragma: 원조 프레임워크 지시어

매크로가 오늘날의 코드생성 유틸리티 같은 것이었다면, #pragma는 현대 지시어의 가장 가까운 조상이었습니다:
#pragma once
이 라인은 C++ 언어 스펙의 일부가 아닙니다. 컴파일러 특정 "힌트"입니다 — 이 파일을 빌드 시스템이 어떻게 취급할지에 영향을 주는 명령어입니다.
  • import가 없습니다
  • 네임스페이스나 출처가 없습니다
  • 소유권 표시가 없습니다
다른 컴파일러는 다른 프래그마를 지원했습니다. 어떤 것은 무시했습니다. 어떤 것은 경고를 냈습니다. 어떤 것은 다르게 동작했습니다.
"컴파일러"를 "번들러"로 바꾸면 2025년입니다.
예를 들어:
  • Next.js는 'use client'에 반응합니다
  • Vite는 무시할 수 있습니다
  • 커스텀 RSC 번들러는 다르게 해석할 수 있습니다
이것이 30년 전에 본 같은 단편화 패턴입니다.

Include Guards: 첫 번째 "보이지 않는 빌드타임 동작"

#pragma once가 일반화되기 전에, C/C++는 include guards를 사용해서 중복 포함을 방지했습니다:
#ifndef MY_HEADER_H
#define MY_HEADER_H

// header contents

#endif
이 패턴:
  • 프로그램 동작을 변경했습니다
  • 도구를 위해서만 존재했습니다
  • 런타임 의미가 없었습니다
  • 빌드 모델의 지식이 필요했습니다, 단순히 언어만이 아니라
이것이 중요합니다:
C/C++는 개발자들에게 "파일에 쓴 코드"와 "언어 자체"가 같은 것이 아니라는 것을 배우도록 강제했습니다.
이것은 고통스러웠지만 변혁적인 교훈이었습니다.
우리는 이제 JavaScript에서 같은 교육적 순간에 있습니다.

다단계 컴파일: 익숙한 파이프라인

C/C++는 구별되는 레이어를 가졌습니다:
전처리기 → 컴파일러 → 링커 → 실행 파일
현대 JS 도구는 같은 분리를 가지고 있습니다, 단지 더 멋진 이름으로:
지시어 스캐너 / 로더 → AST 변환 → 번들러 → 출력 청크
그리고 초보자들이 "C++ 언어"를 전처리기 특이성 때문에 탓했던 것처럼, 오늘 개발자들은:
  • "JavaScript"
  • "React"
  • "플랫폼"
을 탓합니다.
...실제로는 빌드 도구에서 비롯된 동작에 대해.
역사가 유사하게 반복되고 있습니다.

전처리기 시대의 시각을 통한 현대 지시어

C/C++ 평행선을 보면, 현대 JavaScript 지시어가 갑자기 훨씬 더 이해가 됩니다. 이들은 JavaScript 문법의 진화가 아닙니다 — 이들은 컴파일러 힌트의 진화입니다.
매핑을 명시적으로 만들어 봅시다:
전처리기 시대 개념
현대 JS 동등물
#pragma
'use client', 'use server'
#define매크로
코드 변환 / 자동 생성 래퍼
#includeguards
모듈 경계 / 수화 경계
컴파일러 특정 동작
번들러 특정 동작
다단계 빌드
로더 → 변환 → 번들 파이프라인
표면은 기만적으로 단순해 보입니다 — 하지만 실제 작용은 아래에서 일어납니다.

지시어는 실행되지 않습니다 — 도구에 명령합니다

지시어의 핵심 속성은 이것입니다:
이들은 그 자체로는 아무것도 하지 않습니다 — 이들은 다른 무언가가 어떻게 동작할지만 바꿉니다.
여기 지시어 기반 파이프라인에서 일반적으로 일어나는 것의 분석입니다 (단순화되었지만 충분히 정확합니다):
파일 읽기 → 지시어 확인 → 환경 / 경계 결정 →
변환 실행 → 클라이언트/서버 번들 생성 → 출력
C 파이프라인과 비교해 봅시다:
파일 읽기 → 전처리기가 매크로 확장 + 프래그마 처리 →
컴파일 → 링크 → 출력
이들은 구조적으로 동일한 개념입니다, 단지 다른 언어와 시대에 적용된 것일 뿐입니다.

언어 기능의 환상이 지속되는 이유

세 가지 심리적 요인이 혼동에 기여합니다:
  1. 문법처럼 보입니다. #pragma once'use client' 모두 예약 키워드처럼 느껴집니다.
  1. 파일 맨 위에 있습니다. 전체 파일을 형성하는 모든 것은 종종 언어라고 가정됩니다.
  1. 전역적이고 암묵적으로 작용합니다. 인스턴스화나 import가 필요하지 않습니다.
다시 말해, 지시어는 언어적 환상을 성공적으로 악용합니다. 이들은 프로그래밍 언어처럼 작용합니다 — 하지만 하나가 아닙니다.

하지만 현대 지시어는 C pragma보다 더 나아갑니다

여기서 더 흥미로워집니다:
C pragma는 컴파일 동작에만 영향을 주었습니다. 현대 지시어는 종종 실행 모델, 코드 배치, 번들링, 런타임 경계에 영향을 줍니다.
예를 들어, 'use server'는:
  • 코드를 서버 전용 청크로 이동시킬 수 있습니다
  • 호출을 RPC 스텁으로 바꿀 수 있습니다
  • 직렬화 래퍼를 추가할 수 있습니다
  • 데이터 흐름 제약을 강제할 수 있습니다
이것은 이미 C 전처리기가 한 것을 넘어섭니다.
이것은 매크로 시스템 + 컴파일러 패스에 더 가깝습니다.
정확히 이것이 구별이 중요한 이유입니다:
무언가가 런타임 실행과 코드 배치에 영향을 준다면, 우리는 이를 "파일 맨 위의 단순한 문자열"로 취급해서는 안 됩니다.
그런데도, 아이러니하게도, 대부분의 오해는 정확히 그렇게 보이기 때문에 발생합니다.

빌드 도구가 지시어의 실제 해석자입니다

C 컴파일러가 프래그마를 다르게 취급했던 것처럼, 현대 JS 도구도 다양합니다:
  • Next.js는 'use client'를 한 방식으로 해석합니다
  • Vite + RSC 구현은 다른 방식으로
  • 제3자 번들러는 또 다른 방식으로
만약 내일 다른 프레임워크가 다음을 도입했다면:
'use streaming-server';
JavaScript 엔진은 신경 쓰지 않을 것입니다. 도구가 그것이 무엇을 의미하는지 결정할 것입니다.
그리고 그것이 개발자들이 해야 할 인식의 전환입니다:
지시어 ≠ 언어 지시어 = 빌드 명령어
이들은 사용자 영역에 있습니다 — 스펙에 있지 않습니다.
계속 진행하기 전에, C와 C++와의 평행선이 이론적으로 느껴질 수 있다는 점을 잠깐 멈춰서 생각해 봅시다. 실제로 이들을 실제로 본 후에야 더 명확해집니다. 이론을 더 구체적인 초점으로 가져오기 위해, 오늘날의 생태계에서 구체적인 예를 살펴봅시다 — 지시어의 필요성을 드러내는 것이고, 실제 현대 코드에서 이 컴파일러 힌트가 어떻게 나타나는지 보여줍니다.
📦 인라인 서버 함수가 지시어의 필요성을 드러낼 때 저는 한 번 지시어가 왜 필요한지를 완벽하게 드러내는 경우를 마주쳤습니다. 서버 컴포넌트 내부에 정의된 인라인 서버 함수를 생각해 봅시다, 로컬 스코프에서 변수를 캡처하는: 얼핏 보기에, 이것은 일반적인 함수 호출처럼 느껴집니다. 하지만 이것이 작동하려면, 번들러는 미묘하고 필수적인 무언가를 해야 합니다: 런타임 헬퍼는 이를 해결할 수 없습니다. 코드가 실행될 때쯤이면, 클로저는 사라지고 의도는 이미 손실됩니다. 함수는 프로그램이 존재하기 전에 변환되어야 하므로, 클라이언트가 이를 직접 함수로 호출할 수 있습니다, 너무 늦게 함께 꿰맨 프록시가 아니라. 이것이 지시어가 이 공간에 잘 맞는 이유입니다: 이들은 정의 시점에 빌드 도구에 이 함수가 정말 무엇인지를 알려주고, 이에 따라 형성할 시간을 줍니다. 어떤 결정은 코드가 여전히 짜여지고 있을 때 일어나야 합니다 — 이미 살아있은 후가 아니라.

교육적 격차 — 그리고 역사에서 배울 수 있는 것

지시어 주변의 혼동을 한 발 물러서서 보면, 뭔가 놀라운 것을 알아챌 것입니다:
문제는 더 이상 기술적이지 않습니다 — 교육적입니다.
우리는 역사적 패턴을 반복했습니다:
  • 도구 수준의 구성이 언어처럼 보입니다
  • 개발자들이 이것이 언어라고 가정합니다
  • 정신 모델이 잘못됩니다
  • 혼동이 문서가 이를 바로잡을 수 있는 것보다 빠르게 퍼집니다
이것은 C 전처리기에서 일어났습니다. JavaScript 지시어에서 다시 일어나고 있습니다.

우리는 기능이 아니라 레이어링을 가르쳐야 합니다

만약 우리가 'use server'를 다음과 같이 가르친다면:
"이것은 당신의 함수를 서버에서 실행하게 합니다."
...우리는 이미 끝났습니다.
왜냐하면 그 문장은 4개의 별도 레이어를 숨기기 때문입니다:
레이어
책임
문법
문자열 리터럴 쓰기
로더
지시어 감지
빌드 도구
코드 변환 / 분할
런타임
경계 강제
개발자들이 어느 레이어가 무엇을 책임지는지 이해하지 못한다면, 그들은 번들러 문제에 대해 "JavaScript"를 탓할 것입니다 — C 개발자들이 한때 전처리기 버그에 대해 "언어"를 탓했던 것처럼.

C/C++ 커뮤니티가 결국 배운 것

시간이 지나면서, C/C++ 교육이 진화했습니다:
초기 교육: "#define#pragma를 사용하는 방법은 다음과 같습니다."
성숙한 교육: "전처리기가 무엇인지, 그리고 왜 언어와 분리되어 있는지는 다음과 같습니다."
그 전환 후, 혼동이 급격히 떨어졌습니다. 오늘날 진지한 C++ 과정은 먼저 컴파일 단계의 정신 모델을 가르치지 않고 매크로를 가르치지 않습니다.
우리는 JS 지시어에 대해 같은 전환이 필요합니다.

지시어는 나쁘지 않습니다 — 이해되면 강력합니다

이 글은 지시어에 반대하는 주장이 아닙니다. 이들은 목적을 제공합니다:
  • 의도를 선언적으로 전달합니다
  • 보일러플레이트를 줄입니다
  • 도구가 코드를 최적화하고 분리하도록 돕습니다
  • 개발자에게 복잡한 동작을 위한 단순한 스위치를 제공합니다
실제로, 가장 인체공학적인 서버/서버리스 기능 중 일부는 이들 없이는 훨씬 더 번거로울 것입니다.
하지만 인체공학의 가격은 명확성 부채입니다.
마법이 어디서 오는지 가르치지 않으면, 개발자들은 진실의 출처를 잘못 귀속시킵니다 — 그리고 디버깅이 붕괴됩니다.

내일부터 가르칠 수 있는 단순한 멘탈 모델

저는 지시어를 한 문장으로 설명하는 것을 좋아합니다:
"지시어는 당신의 빌드 도구를 위해 남기는 메모입니다 — JavaScript를 위한 것이 아닙니다."
그 한 줄만으로도 70%의 오해를 해결합니다.
하나의 비유를 추가하세요:
"#pragma once가 C++ 문법이 아니었다면, 'use client'도 오늘날 JavaScript 문법이 아닙니다."
그리고 갑자기, 사람들이 이해합니다.
이것은 더 많은 문서를 필요로 하지 않습니다 — 더 나은 프레이밍을 필요로 합니다.

명확성을 향한 실질적인 단계: 지시어를 위한 TypeScript 플러그인

마무리하기 전에, 실제 코드베이스에서 이 혼동을 줄이기 위해 제가 취한 한 가지 구체적인 단계를 공유하고 싶습니다. 문제의 일부가 디시어를 언어처럼 보이지만 공식적인 구조가 없다는 것에서 온다면, 이들에게 타입 수준의 의미를 부여하는 것이 격차를 메우는 한 가지 방법입니다.
저는 typescript-plugin-directives라는 작은 TypeScript 플러그인을 만들었는데, 이는 지시어에 타입 안전성과 IntelliSense 인식을 가져옵니다. 이것은 팀이 다음을 할 수 있게 합니다:
  • 자신의 지시어 어휘를 정의하고,
  • 컴파일 시간에 검증하고,
  • 편집기 힌트와 자동완성을 얻고,
  • 'use clinet' 같은 조용한 오타를 피합니다.
목표는 지시어를 "표준화"하는 것이 아니라, 개발자와 도구 모두에게 이들의 의도를 명시적이고 가시적으로 만드는 것입니다 — 번들러가 먼저 이들을 해석할 필요 없이.
여기서 시도할 수 있습니다:
의도적으로 가볍습니다 — 멘탈 모델이 더 일찍 클릭되도록 돕기 위한 작은 레이어일 뿐이고, TypeScript 프로젝트 내에서 지시어에 더 공식적인 형태를 제공합니다.
TypeScript 플러그인이 지시어를 "강제"할 수 있다는 일반적인 가정이 있습니다 — 플러그인이 이들을 알면, 시스템이 기본적으로 안전해진다는 것입니다. 하지만 TS 플러그인은 Language Service에 있습니다. 이것은 인식, 경고, 지침을 제공할 수 있습니다 — 그런데도 코드가 실제로 변환되는 곳에서 실행되지 않습니다. 컴파일이나 번들링에 참여하지 않습니다. 플러그인은 지시어 사용을 식별하는 데 도움이 될 수 있지만, 이들의 의미론을 강제할 수 없습니다. 그를 위해, 컴파일러는 의도의 신호가 필요합니다 — 타입 시스템이 이해할 수 있는 무언가. 이를 가능하게 하기 위해, 저는 플러그인에서 전역 Directive 타입을 노출합니다. 저자들은 satisfies를 사용할 수 있습니다 — 편집기에 알리기 위해서가 아니라, TypeScript 컴파일러에 알리기 위해 주어진 문자열이 지시어로 의도되었다는 것을: 'use server' satisfies Directive; 이것은 동작이나 변환을 트리거하지 않습니다. 그것이 하는 것은 훨씬 더 기본적이고 근본적입니다: 타입 시스템에 알립니다: "이것을 지시어로 취급하세요 — 그리고 그렇게 검증하세요." 이것은 의도를 런타임이 아닌 컴파일러와 정렬하고, IDE뿐만 아니라. 인라인 서버 함수를 컴포넌트 밖으로 끌어올리고, 캡처된 스코프를 분석하고, 직접 호출 가능한 경계를 생성하는 실제 분리 — 이것은 여전히 전적으로 빌드에 속합니다. 타입 시스템은 의도를 인정할 수 있지만, 번들러가 이에 작용해야 합니다. 한 가지 실질적인 주의: 오늘날, Next.js를 포함한 일부 도구는 지시어가 벌거벗은 문자열 리터럴로 나타나기를 기대합니다. 'use server' satisfies Directive로 쓰여질 때, 지시어는 더 이상 감지되지 않을 수 있습니다, 리터럴이 더 이상 프레임워크가 스캔하는 정확한 형태가 아니기 때문입니다. 이것이 변할 때까지, 이 패턴은 Next.js에 의해 선택되지 않을 것입니다. 한 가지 더 미묘한 점이 있습니다. 이 타입 수준의 의도는 타입 체커가 실제로 실행되는 경우에만 중요합니다. 많은 현대 도구 체인 — esbuild, SWC, Oxc, Bun, 심지어 대부분의 Deno와 Vite 설정 — 전혀 타입 체크하지 않습니다. 이들은 단순히 타입을 제거하고 계속합니다. 그 환경에서, Directive + satisfies 표현은 듣기 위해 기회를 가지지 못한 컴파일러에 대한 조용한 메모가 됩니다.

마무리 성찰

Tanner의 글은 가치 있는 의문을 제기합니다 — 나쁜 습관이 굳어지기 전에 일찍 가질 가치가 있는 것입니다. 저는 지시어를 제거해야 한다고 믿지 않습니다; 이들은 명확하게 실제 문제를 해결합니다. 하지만 우리는 역사에서 배울 수 있고, 전체 C/C++ 개발자 세대가 배워야 했던 혼동을 피할 수 있습니다.
우리는 이전에 여기 있었습니다. 우리는 이 이야기가 어떻게 진행되는지 압니다. 이번에는, 중간의 혼동의 십 년을 건너뛸 수 있습니다.
레이어를 가르치세요. 유래를 가르치세요. 멘탈 모델을 가르치세요.
그러면 지시어는 "비밀 언어 기능"처럼 느껴지지 않을 것입니다 — 그리고 이들이 실제로 무엇인지, 강력하고 의도적인 컴파일러 힌트처럼 느껴지기 시작할 것입니다.

0
94

댓글

?

아직 댓글이 없습니다.

첫 번째 댓글을 작성해보세요!

Inkyu Oh님의 다른 글

더보기

유사한 내용의 글