PHpullh

JAVASCRIPT · 심층 가이드

JavaScript 패턴 완전 정리

전략, 옵저버, 빌더 같은 고전 디자인 패턴을 클래스 대신 클로저와 고차 함수로 다시 쓴 20편으로 JavaScript다운 설계 감각을 잡습니다.

주제 20개 · 예제 코드 포함 · 최종 수정 2026-09-03 · 작성 pullh 편집팀

고전 디자인 패턴 목록은 대부분 Java 시절의 제약 위에서 만들어졌습니다. 함수가 일급 값이고 객체 형태를 실행 중에 바꿀 수 있는 JavaScript에서는 같은 문제가 훨씬 작은 코드로 풀립니다. 전략 패턴은 함수를 인자로 넘기면 끝이고, 데코레이터는 함수를 감싸는 함수 하나면 됩니다. 그래서 여기서는 UML 구조를 옮기기보다 패턴이 해결하려던 문제가 무엇이었는지에 초점을 둡니다.

설정 객체부터 보면 나머지가 읽힙니다. 옵션을 객체 하나로 받는 습관이 빌더 패턴과 팩터리 메서드의 출발점이기 때문입니다. 전략 패턴·데코레이터·미들웨어 체인은 사실상 같은 계열로, 함수를 바꿔 끼우느냐 감싸느냐 줄줄이 엮느냐의 차이입니다. 데이터 접근 쪽은 저장소 패턴과 데이터 매퍼를 짝으로 읽어야 경계가 보이고, 캐시 래퍼와 retry 유틸은 그 위에 얹는 층입니다.

옵저버에서 가장 많이 새는 곳은 구독 해제입니다. 등록한 리스너를 떼어 내지 않으면 대상 객체가 콜백을, 콜백이 클로저를, 클로저가 다시 큰 객체를 붙잡아 메모리가 눌러앉습니다. 구독 함수가 해제 함수를 돌려주도록 만드는 것이 가장 안전한 계약입니다. 의존성 주입 컨테이너도 마찬가지로 순환 참조를 만들기 쉬우니, 등록 시점이 아니라 해석 시점에 순환을 감지하도록 짜 두는 편이 낫습니다.

01설정 객체

할인 계산 함수 두 개를 percent와 flat이라는 키에 담아 두고 필요한 것을 꺼내 실행합니다. 분기마다 if를 늘리는 대신 표를 만들어 두는 방식이라 항목 추가가 간단합니다. 키를 사용자 입력으로 받는다면 없는 이름이 들어올 때 undefined를 호출하게 되므로 사전 확인이 필요합니다.

JavaScript code

// 설정 객체
const strategies = { percent: (value) => value * 0.9, flat: (value) => value - 5 };
console.log(strategies.percent(100));
알아두면 좋은 점

동작을 표로 두면 새 항목을 더할 때 기존 분기를 건드리지 않아도 되고, 지원 목록을 Object.keys로 그대로 뽑아 화면에 보여 줄 수도 있습니다.

자주 하는 실수

strategies[name]이 없을 때 곧바로 호출하면 함수가 아니라는 오류가 납니다. 키를 외부에서 받는다면 존재 여부를 먼저 확인해야 합니다.

02전략 패턴

listeners 배열에 함수를 등록하고 publish가 등록된 전부를 순회하며 값을 넘깁니다. 실행할 동작을 바깥에서 끼워 넣는다는 점은 전략 선택과 닮았지만, 하나를 고르는 대신 모두를 부른다는 차이가 있습니다. 구독을 해제할 수단이 없어 배열이 계속 자라면 그대로 누수가 됩니다.

JavaScript code

// 전략 패턴
const listeners = [];
const subscribe = (fn) => listeners.push(fn);
const publish = (value) => listeners.forEach(fn => fn(value));
subscribe(console.log);
publish("update-2");
알아두면 좋은 점

구독 함수가 해제용 함수를 돌려주게 만들면 등록한 쪽이 자기 구독만 정확히 끊을 수 있어, 배열에서 인덱스를 찾아 지울 필요가 없습니다.

자주 하는 실수

구독을 해제할 수단이 없으면 리스너 배열이 계속 자라고 그 함수가 참조하는 객체까지 회수되지 않습니다. 화면을 오가는 코드에서 흔한 누수입니다.

03옵저버

Map에 값이 있으면 그것을, 없으면 새 객체를 만들어 저장한 뒤 돌려주는 getUser입니다. 같은 id로 몇 번을 불러도 항상 동일한 인스턴스를 받게 됩니다. 반환된 객체를 호출부가 수정하면 캐시에 든 원본까지 함께 바뀐다는 점이 함정입니다.

JavaScript code

// 옵저버
const cache = new Map();
const getUser = (id) => { if (!cache.has(id)) cache.set(id, { id, name: "user" }); return cache.get(id); };
console.log(getUser(3));
알아두면 좋은 점

Map은 has로 존재를 먼저 확인할 수 있어, 값이 undefined로 저장된 항목과 아예 없는 항목을 구분할 수 있습니다.

자주 하는 실수

캐시에서 꺼낸 객체를 호출부가 고치면 보관 중인 원본이 함께 바뀝니다. 공유가 곤란하다면 사본을 돌려주거나 동결해 내보내야 합니다.

04데코레이터

미들웨어 두 개를 순서대로 실행하되, next()가 다음 함수를 부르는 재귀 구조로 이어 붙입니다. index 비교가 같은 단계에서 next()를 두 번 부르는 실수를 예외로 잡아냅니다. 원래 동작 앞뒤를 감싼다는 점에서 데코레이터와 발상이 같고, 실행 흔적은 ctx.steps에 누적됩니다.

JavaScript code

// 데코레이터
const middlewares = [(ctx, next) => { ctx.steps.push("a"); return next(); }, (ctx, next) => { ctx.steps.push("b"); return next(); }];
const ctx = { steps: [] };
let index = -1;
const run = (i) => { if (i <= index) throw new Error("twice"); index = i; const mw = middlewares[i]; if (mw) return mw(ctx, () => run(i + 1)); };
run(0);
console.log(ctx.steps);
알아두면 좋은 점

next()의 반환값을 그대로 돌려주면 마지막 단계의 결과가 첫 호출까지 이어져, 체인 전체를 하나의 값으로 다룰 수 있습니다.

자주 하는 실수

미들웨어에서 next()를 부르지 않고 끝내면 이후 단계가 통째로 건너뛰어집니다. 조건 분기 안에서 호출을 빠뜨리면 특정 경로만 조용히 멈춥니다.

05어댑터

서로 다른 계산 방식을 값 하나를 받아 값 하나를 내는 동일한 호출 모양으로 통일해 한 객체에 모아 둡니다. 형태가 제각각인 외부 로직을 한 규격에 맞춰 넣을 때 이 구조가 어댑터 역할을 합니다. 화살표 함수는 자체 this가 없어, 객체 메서드로 두어도 this로 형제 키에 접근할 수 없습니다.

JavaScript code

// 어댑터
const strategies = { percent: (value) => value * 0.9, flat: (value) => value - 5 };
console.log(strategies.percent(100));
알아두면 좋은 점

호출 모양을 통일해 두면 나중에 구현을 교체할 때 호출부를 고치지 않아도 되고, 새 구현이 규격을 지키는지도 같은 테스트로 확인할 수 있습니다.

자주 하는 실수

객체 리터럴 안의 화살표 함수에서 this로 형제 키를 부르면 this가 그 객체가 아니라 바깥을 가리켜 호출이 실패합니다.

06커맨드

subscribe로 실행할 함수를 모아 두고 나중에 publish 시점에 인자를 실어 한꺼번에 호출합니다. 무엇을 할지를 함수 값으로 저장해 두었다가 원하는 때 실행한다는 점이 커맨드의 핵심입니다. forEach는 예외를 삼키지 않으므로 한 리스너가 실패하면 뒤쪽 리스너는 아예 실행되지 않습니다.

JavaScript code

// 커맨드
const listeners = [];
const subscribe = (fn) => listeners.push(fn);
const publish = (value) => listeners.forEach(fn => fn(value));
subscribe(console.log);
publish("update-6");
알아두면 좋은 점

실행할 동작을 값으로 저장해 두면 큐에 쌓았다가 나중에 처리하거나, 실행 기록을 남겨 되돌리기 기능을 붙이기도 쉬워집니다.

자주 하는 실수

forEach는 콜백의 예외를 그대로 밖으로 던지므로 리스너 하나가 실패하면 뒤쪽 리스너는 아예 실행되지 않습니다. 각 호출을 개별로 감싸야 합니다.

07상태 패턴

cache라는 Map이 id별 객체를 들고 있고, 최초 조회 때만 만들고 이후에는 보관 중인 것을 꺼냅니다. 현재 값을 한곳에 모아 두고 조회 시점마다 그 값을 얻는다는 점에서 상태 보관소에 가깝습니다. Map은 키를 === 기준으로 구분하므로 숫자 3과 문자열 '3'은 별개 항목이 됩니다.

JavaScript code

// 상태 패턴
const cache = new Map();
const getUser = (id) => { if (!cache.has(id)) cache.set(id, { id, name: "user" }); return cache.get(id); };
console.log(getUser(7));
알아두면 좋은 점

상태를 한곳에 모아 두면 어디서 읽어도 같은 값을 보게 되지만, 그만큼 어느 코드가 그 값을 바꿨는지도 좁혀 두어야 추적이 가능해집니다.

자주 하는 실수

Map의 키는 엄격 비교로 구분되어 숫자 3과 문자열 "3"이 서로 다른 항목이 됩니다. URL에서 읽은 id를 그대로 쓰면 캐시가 두 벌 생깁니다.

08팩터리 메서드

배열에 담긴 함수들을 run(i)이 인덱스로 찾아 실행하고, 각 함수는 next라는 새 클로저를 넘겨받습니다. 단계마다 다음 실행을 감싼 함수가 새로 만들어지는 부분이 사실상 함수를 찍어 내는 자리입니다. 배열 끝을 넘으면 꺼낸 값이 undefined라 아무 일도 없이 조용히 끝납니다.

JavaScript code

// 팩터리 메서드
const middlewares = [(ctx, next) => { ctx.steps.push("a"); return next(); }, (ctx, next) => { ctx.steps.push("b"); return next(); }];
const ctx = { steps: [] };
let index = -1;
const run = (i) => { if (i <= index) throw new Error("twice"); index = i; const mw = middlewares[i]; if (mw) return mw(ctx, () => run(i + 1)); };
run(0);
console.log(ctx.steps);
알아두면 좋은 점

단계마다 다음 실행을 감싼 클로저가 새로 만들어져 각자 자기 위치를 기억하므로, 실행기가 별도 상태를 들고 다니지 않아도 체인이 이어집니다.

자주 하는 실수

배열 끝을 넘으면 꺼낸 값이 undefined라 아무 일 없이 끝납니다. 마지막에 실제 처리를 두었다면 호출되지 않은 것을 눈치채기 어렵습니다.

09빌더 패턴

완성된 함수 두 개를 한 객체에 나열해 두고 필요한 키를 골라 부르는 형태입니다. 빌더처럼 단계별로 조립하지는 않고, 가능한 결과를 미리 늘어놓은 쪽에 가깝습니다. 조합해야 할 옵션이 곱으로 늘어나면 이런 나열식은 항목 수가 폭발하므로 조합용 함수를 두는 편이 낫습니다.

JavaScript code

// 빌더 패턴
const strategies = { percent: (value) => value * 0.9, flat: (value) => value - 5 };
console.log(strategies.percent(100));
알아두면 좋은 점

선택지를 미리 나열하는 방식은 목록이 짧고 고정일 때 가장 읽기 쉽습니다. 조합이 늘기 시작하면 옵션을 받아 함수를 만들어 내는 쪽으로 옮길 신호입니다.

자주 하는 실수

가능한 조합을 전부 키로 늘어놓으면 옵션이 하나 늘 때마다 항목 수가 배로 불어나고, 빠뜨린 조합은 호출 시점에야 드러납니다.

10캐시 래퍼

리스너 배열과 등록, 발행 세 조각으로 이루어진 최소한의 발행 구독 코드이며 캐시는 없습니다. 캐시를 얹는다면 마지막으로 발행한 값을 보관했다가 늦게 구독한 쪽에 즉시 흘려주는 식이 됩니다. 지금 구조에서는 구독 이전에 발행된 값을 되살릴 방법이 전혀 없습니다.

JavaScript code

// 캐시 래퍼
const listeners = [];
const subscribe = (fn) => listeners.push(fn);
const publish = (value) => listeners.forEach(fn => fn(value));
subscribe(console.log);
publish("update-10");
알아두면 좋은 점

마지막 값을 보관해 두었다가 늦게 구독한 쪽에 즉시 전달하면, 구독 시점 때문에 초기 상태를 놓치는 문제를 없앨 수 있습니다.

자주 하는 실수

발행이 구독보다 먼저 일어나면 그 값은 영영 사라집니다. 초기화 순서에 기대는 코드는 환경이 조금만 바뀌어도 동작이 달라집니다.

11retry 유틸

재시도 로직 대신, Map에 값이 없을 때만 새로 만들어 채우는 조회 함수가 들어 있습니다. 재시도 유틸도 골격은 같고 판단 조건이 존재 여부에서 예외 발생 여부로 바뀔 뿐입니다. 다만 재시도에서는 대기 시간을 지수적으로 늘려야 실패한 대상 서버를 계속 몰아붙이지 않습니다.

JavaScript code

// retry 유틸
const cache = new Map();
const getUser = (id) => { if (!cache.has(id)) cache.set(id, { id, name: "user" }); return cache.get(id); };
console.log(getUser(11));
알아두면 좋은 점

재시도 간격을 지수적으로 늘리면서 무작위 값을 섞으면, 여러 클라이언트가 같은 시점에 한꺼번에 몰려 재시도하는 상황을 피할 수 있습니다.

자주 하는 실수

무엇을 재시도할지 가리지 않으면 잘못된 요청처럼 다시 보내도 결과가 같은 실패까지 반복합니다. 재시도해도 되는 오류만 골라내야 합니다.

12미들웨어 체인

ctx를 공유한 채 두 단계를 순서대로 거치며 steps 배열에 흔적을 남깁니다. index 가드가 이미 지나간 단계로 되돌아가는 호출을 throw로 막습니다. next()의 반환값을 그대로 되돌려 주기 때문에, 미들웨어 하나가 Promise를 반환하면 체인 전체가 비동기로 이어집니다.

JavaScript code

// 미들웨어 체인
const middlewares = [(ctx, next) => { ctx.steps.push("a"); return next(); }, (ctx, next) => { ctx.steps.push("b"); return next(); }];
const ctx = { steps: [] };
let index = -1;
const run = (i) => { if (i <= index) throw new Error("twice"); index = i; const mw = middlewares[i]; if (mw) return mw(ctx, () => run(i + 1)); };
run(0);
console.log(ctx.steps);
알아두면 좋은 점

index 가드는 같은 단계에서 next()를 두 번 부르는 실수를 즉시 예외로 드러내, 이후 단계가 중복 실행되는 것을 막아 줍니다.

자주 하는 실수

체인 중간에 비동기 단계가 섞이면 반환된 프로미스를 기다리지 않는 호출부가 완료 전에 다음 코드로 넘어갑니다. 시작 지점에서 기다려야 합니다.

13검증기 조합

값 하나를 받아 값 하나를 돌려주는 작은 함수들을 이름표와 함께 모아 둔 구조입니다. 검증기도 같은 방식으로 규칙 함수를 모은 뒤 필요한 것만 골라 순서대로 적용합니다. 검증에서는 첫 실패에서 멈출지 모든 오류를 모을지를 먼저 정해야 하고, 그 선택이 반환 타입 자체를 바꿉니다.

JavaScript code

// 검증기 조합
const strategies = { percent: (value) => value * 0.9, flat: (value) => value - 5 };
console.log(strategies.percent(100));
알아두면 좋은 점

검증 규칙을 값 하나를 받는 함수로 통일하면 목록에서 필요한 것만 골라 순서대로 적용할 수 있고, 같은 규칙을 다른 화면에서 재사용하기도 쉽습니다.

자주 하는 실수

첫 실패에서 멈출지 모든 오류를 모을지 정하지 않으면 규칙마다 반환 타입이 달라집니다. 나중에 합치려 할 때 호출부를 전부 고치게 됩니다.

14포매터 파이프

등록된 함수 목록을 순회하며 같은 입력값을 각각에게 그대로 전달합니다. 파이프라면 앞 함수의 출력이 뒤 함수의 입력이 되어야 하므로 반환값을 이어받는 reduce 형태로 바꿔야 합니다. 지금 코드는 각 함수가 무엇을 반환하든 전부 버립니다.

JavaScript code

// 포매터 파이프
const listeners = [];
const subscribe = (fn) => listeners.push(fn);
const publish = (value) => listeners.forEach(fn => fn(value));
subscribe(console.log);
publish("update-14");
알아두면 좋은 점

파이프로 바꾸려면 앞 함수의 결과를 다음 인자로 넘기는 reduce 형태여야 하고, 각 함수는 반드시 값을 돌려줘야 사슬이 끊기지 않습니다.

자주 하는 실수

지금 구조는 등록된 함수의 반환값을 전부 버려서, 문자열을 다듬어 돌려주는 포맷 함수를 넣어도 아무 효과 없이 조용히 지나갑니다.

15페이지네이션

id를 키로 하는 Map에서 항목을 하나씩 꺼내며, 없으면 즉석에서 만들어 채워 넣습니다. 목록을 페이지 단위로 끊어 읽을 때도 이미 받아 둔 항목은 이렇게 재조회를 건너뛸 수 있습니다. 다만 목록 중간에 항목이 추가되면 id별 캐시는 그대로여도 페이지 경계가 밀려 중복이나 누락이 생깁니다.

JavaScript code

// 페이지네이션
const cache = new Map();
const getUser = (id) => { if (!cache.has(id)) cache.set(id, { id, name: "user" }); return cache.get(id); };
console.log(getUser(15));
알아두면 좋은 점

커서 기반으로 다음 페이지를 요청하면 목록 중간에 항목이 추가되어도 경계가 밀리지 않아 중복이나 누락이 생기지 않습니다.

자주 하는 실수

오프셋으로 페이지를 나누는 동안 앞쪽 항목이 삭제되면 뒤 페이지가 한 칸씩 당겨져, 사용자가 한 번도 보지 못한 항목이 생깁니다.

16데이터 매퍼

각 미들웨어가 공용 ctx를 받아 steps에 항목을 밀어 넣는 식으로 결과를 채워 갑니다. 원본을 단계별로 변환해 목적지 구조를 완성하는 매퍼도 같은 골격을 씁니다. 여러 단계가 하나의 객체를 직접 고치므로, 어느 단계가 어떤 필드를 채웠는지는 전부 읽어 보기 전에는 알 수 없습니다.

JavaScript code

// 데이터 매퍼
const middlewares = [(ctx, next) => { ctx.steps.push("a"); return next(); }, (ctx, next) => { ctx.steps.push("b"); return next(); }];
const ctx = { steps: [] };
let index = -1;
const run = (i) => { if (i <= index) throw new Error("twice"); index = i; const mw = middlewares[i]; if (mw) return mw(ctx, () => run(i + 1)); };
run(0);
console.log(ctx.steps);
알아두면 좋은 점

각 단계가 새 객체를 돌려주게 하면 어느 단계가 어떤 필드를 채웠는지 중간 결과만 비교해도 알 수 있어 디버깅이 쉬워집니다.

자주 하는 실수

여러 단계가 공용 객체를 직접 고치면 나중 단계가 앞 단계의 값을 덮어써도 아무 신호가 없습니다. 필드 이름이 겹치는 순간 조용히 결과가 달라집니다.

17저장소 패턴

키로 구현을 찾아 꺼내 쓰는 조회용 객체를 만들고 그중 하나를 실행합니다. 저장소도 이름으로 구현이나 데이터를 찾아 주는 계층이라는 점에서 뼈대가 겹칩니다. 객체 리터럴을 사전처럼 쓸 때는 toString 같은 프로토타입 키가 이미 있는 것처럼 보이므로, Object.create(null)이나 Map이 더 안전합니다.

JavaScript code

// 저장소 패턴
const strategies = { percent: (value) => value * 0.9, flat: (value) => value - 5 };
console.log(strategies.percent(100));
알아두면 좋은 점

Object.create(null)로 만든 객체는 프로토타입이 없어 in 검사나 대괄호 조회에 내장 이름이 끼어들지 않습니다.

자주 하는 실수

일반 객체를 사전처럼 쓰면 obj["toString"]이 함수로 잡혀 등록하지 않은 키가 있는 것처럼 보입니다. 사용자 입력을 키로 받을 때 특히 위험합니다.

18의존성 주입 컨테이너

함수를 배열에 등록해 두었다가 발행 시점에 꺼내 부르는, 아주 얇은 등록소입니다. 컨테이너 역시 이름으로 생성 함수를 등록하고 요청이 오면 꺼내 실행한다는 점에서 구조가 닮았습니다. 다만 컨테이너는 같은 요청에 같은 인스턴스를 줄지 매번 새로 만들지를 정해야 하는데, 이 코드에는 그런 수명 개념이 아예 없습니다.

JavaScript code

// 의존성 주입 컨테이너
const listeners = [];
const subscribe = (fn) => listeners.push(fn);
const publish = (value) => listeners.forEach(fn => fn(value));
subscribe(console.log);
publish("update-18");
알아두면 좋은 점

컨테이너를 만들 때는 요청마다 새 인스턴스를 줄지 하나를 공유할지 정해야 하고, 그 수명 정책이 상태를 가진 객체에서는 동작 차이로 그대로 이어집니다.

자주 하는 실수

이름 문자열로 의존을 찾으면 오타가 실행 시점까지 드러나지 않습니다. 등록되지 않은 이름은 undefined가 되어 호출 순간 오류로 터집니다.

19템플릿 메서드

getUser는 캐시 확인, 생성, 저장, 반환이라는 고정된 순서를 함수 안에 박아 두고 있습니다. 템플릿 메서드로 가려면 그중 생성 단계만 인자로 받은 함수에 넘겨 바꿀 수 있게 열면 됩니다. 그렇게 나누면 키 계산과 생성 로직을 다른 곳에서 관리하게 되므로 키가 겹치는 상황을 따로 챙겨야 합니다.

JavaScript code

// 템플릿 메서드
const cache = new Map();
const getUser = (id) => { if (!cache.has(id)) cache.set(id, { id, name: "user" }); return cache.get(id); };
console.log(getUser(19));
알아두면 좋은 점

고정된 절차 중 바뀌는 단계만 함수 인자로 열어 두면, 캐시 골격은 그대로 둔 채 생성 로직만 상황에 맞게 갈아 끼울 수 있습니다.

자주 하는 실수

키 계산을 바깥으로 빼면 서로 다른 대상이 같은 키를 만들 수 있습니다. 캐시가 엉뚱한 값을 돌려줘도 오류가 나지 않아 발견이 늦어집니다.

20파사드

호출부는 시작 함수 하나만 부르고, 내부의 인덱스 관리와 다음 단계 연결은 전부 감춰져 있습니다. 복잡한 실행 절차를 함수 하나 뒤로 숨긴다는 점이 파사드가 하는 일과 같습니다. 대신 오류가 나면 스택에 재귀 호출이 단계 수만큼 겹쳐 찍혀서 어느 단계가 원인인지 눈으로 찾기 번거롭습니다.

JavaScript code

// 파사드
const middlewares = [(ctx, next) => { ctx.steps.push("a"); return next(); }, (ctx, next) => { ctx.steps.push("b"); return next(); }];
const ctx = { steps: [] };
let index = -1;
const run = (i) => { if (i <= index) throw new Error("twice"); index = i; const mw = middlewares[i]; if (mw) return mw(ctx, () => run(i + 1)); };
run(0);
console.log(ctx.steps);
알아두면 좋은 점

복잡한 절차를 함수 하나 뒤로 숨기면 호출부가 내부 구조에 의존하지 않게 되어, 안쪽을 바꿔도 바깥 코드를 고칠 필요가 없습니다.

자주 하는 실수

재귀로 단계를 잇는 구조는 오류 스택에 같은 함수가 단계 수만큼 겹쳐 찍혀, 실제로 문제가 난 미들웨어를 눈으로 골라내기 번거롭습니다.

정리하며

  • 전략과 데코레이터는 클래스 계층 없이 함수 인자와 래핑만으로 충분합니다
  • 구독 함수는 항상 해제 함수를 반환하도록 만들어 리스너 누수를 막습니다
  • 옵션이 세 개를 넘어가면 위치 인자 대신 설정 객체 하나로 받습니다
  • 저장소 패턴으로 도메인 로직과 데이터 접근 코드의 경계를 고정합니다

더 깊이 들어가고 싶다면 JavaScript 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.