any는 자바스크립트에 존재하는 모든 값을 오류 없이 받을 수 있다. 즉 자바스크립트의 기본 사용방식과 같으므로 타입을 명시하지 않은 것과 같은 효과. 즉 타입스크립트에서의 any는 지양해야 하는 패턴이다. 그럼에도 불구하고 any를 사용해야 하는 경우가 있다.
1. 개발 단계에서 임시로 값을 지정해야 할 경우
any와 유사한 모든 타입의 값이 할당 될 수 있다.
하지만 다른 타입으로 선언된 변수에는 unknown 타입의 값을 할당 할 수 없다.
any 타입의 값에 unknown을 넣을수 있으나 다른 타입의 값에 unknown 타입의 값을 넣을 수는 없다.
unknown은 타입스크립트 3.0때 릴리즈 됨. 기존 타입시스템의 부족한 점을 보완하기 위해서 등장. unknown을 할당 시에 에러가 발생하지 않으나 런타임 시에 에러가 발생했다.
짧게 이야기 하면 상위에서 타입을 알 수 없지만, 자식(하위) 컴포넌트나 함수 내부에서 타입을 좁혀가며 안전하게 사용하기 위해서 만든 타입이라고 예상됨
값을 반환 할 수 없는 경우
1. 에러를 던지는 경우
자바스크립트에서는 런타임에 의도적으로 에러를 발생시키고 캐치 할 수 있다. 이는 값을 반환하는 것으로 간주 하지 않는다. 이때 never 타입이다.
2. 무한히 함수가 실행되는 경우
무한 루프일 경우 함수가 종료되지 않기 때문에 값을 반환하지 못한다.
타입스크립트에서 조건부 타입을 결정 할 때 특정 조건을 만족하지 않는 경우에 엄격한 타입 검사 목적으로 never를 사용하기도 한다.
선언방식
1.[](대괄호)형식을 사용하여 배열타입을 선언const array1:number[]: [1,2,3]
Array키워드를 통해서 배열 타입을 선언const array2:Array<number> = [1,2,3]
이 두가지 방식이 존재하며, 여러 타입을 관리 할 경우 유니온 타입을 사용 할 수 있다.
유니온 타입을 사용할 경우
const array1:number[] | string[]: [1,2,"text"] const array2:Array<number | string> = [1,2,"text"] const array1:(number|string)[]: [1,2,"text"]
타입스크립트는 배열타입을 명시하는 것만으로는 길이를 제한 할 수 없다. 튜플은 배열의 하위 타입으로 기존의 타입스크립트의 배열 기능에 길이 제한 까지 추가한 타입 시스템이라고 볼 수 있다.
튜플 타입
const tuple1:[number] = [1] // 길이는 한개이며 요소로는 숫자타입 제한 const tuple2:[number, string, boolean] = [1,"text", false] // 여러 타입 혼합 가능
enum ProgrammingLanguage { Typescript, // 0 Javascript, // 1 Java, // 2 Python, // 3 Kotlin, // 4 Rust, // 5 Go, // 6 } // 각 멤버에게 접근하는 방식은 자바스크립트에서 객체의 속성에 접근하는 방식과 동일하다 ProgrammingLanguage.Typescript; // 0 ProgrammingLanguage.Rust; // 5 ProgrammingLanguage["Go"]; // 6 // 또한 역방향으로도 접근이 가능하다 ProgrammingLanguage[2]; // "Java"
enum ProgrammingLanguage { Typescript = "Typescript", Javascript = "Javascript", Java = 300, Python = 400, Kotlin, // 401 Rust, // 402 Go, // 403 }
enum ItemStatusType { DELIVERY_HOLD = "DELIVERY HOLD", // 배송 보류 DELIVERY_READY = "DELIVERY READY", // 배송 준비 중 DELIVERING = "DELIVERING", // 배송 중 DELIVERED = "DELIVERED", // 배송 완료 } const checkItemAvailable = (itemStatus: ItemStatusType) => { switch (itemStatus) { case ItemStatusType.DELIVERY_HOLD: case ItemStatusType.DELIVERY_READY: case ItemStatusType.DELIVERING: return false; case ItemStatusType.DELIVERED: default: return true; } };
checkitemAvailable 함수의 인자인 itemStatus는 ItemStatusType 열거형을 타입으로 가진다. 이를 통해 얻을 수 있는 효과는 itemstatus의 타입이 문자열로 지정된 경우와 비교 했을 때 다음과 같다.
열거형은 관련이 높은 멥러를 모아 문자열 상수처럼 사용하고자 할 때 유용하게 사용 할 수 있다. 하지만 열거형을 사용할 때 주의해야 할 점은 숫자로만 이루어져 있거나 타입스크립트가 자동으로 추론한 열거형은 안전하지 않은 결과를 낳을 수 있다.
이런한 동작을 막기 위해서 const enum으로 열거형을 선언하는 방법이 있다. 이 방식은 역방향으로의 접근을 허용하지 않기 때문에 자바스크립트에서 객체에 접근하는 것과 유사한 동작을 보장한다.
ProgrammingLanguage[200] // undefined 를 출력하지만 별다른 에러를 발생시키지 않는다. // 아래와 같이 선언 하면 위와 같은 문제를 해결 할 수 있다. const enum ProgrammingLanguage { // .... }
그러나 이런 const enum 으로도 숫자 상수로 관리되는 열거형은 선언한 값 이외의 값을 할당하거나 접근할 때 이를 막지 못한다. 반면 문자열 상수 방식으로 선언한 열거형은 미리 선언하지 않은 멤버로의 접근을 방지한다. 따라서 문자열 상수 방식으로 열거형을 사용하는 것이 숫자 상수 방식보다 안저나며 의도하지 않은 접근을 방지하는데 도움이 된다.
여러가지 타입을 결합하여 하나의 단일 타입으로 만들 수 있다. 다시 말해 기존에 존재하는 여러가지 다른 타입들을 합쳐서 해당 타입의 모든 멤버를 가지는 새로운 타입을 생성하는 것. 교차 타입은 &을 사용해서 표기한다.
export type User = { id: number; email: string; nickname?: string; image?: string; createdAt: string; updatedAt: string; }; export type PostUsers = Pick<User, "email" | "nickname"> & { password: string; };
교차 타입이 a와 b를 모두 만족하는 경우라면 유니온 타입은 a또는 b중 하나가 될수 있는 타입.
export type PaginationParams = { size?: number; // 한번에 불러오는 갯수 page?: number; // 부르는 페이지 sort?: string; // 데이터 정렬방식 direction?: "desc" | "asc"; // 최신순 오래된순 };
특정 타입의 속성 이름은 알 수 없지만 속성값의 타입을 알고 잇을 대 사용하는 문법
인터페이스의 내부에 [Key: K]: T 형식으로 명시해주면 되는데, 이는 해당 타입의 속성 키는 모두 K 타입이어야 하고 속성값은 모두 T 타입을 가져야 한다는 의미이다.
export type IndexSignatures = { [key: string]: number|boolean; length: number; isValid: boolean; name:string; // number이거나 boolean 타입이 아니므로 에러발생 }
다른 타입의 특정 속성이 가지는 타입을 조회하기 위해서 사용.
type Example = { a: number; b: string; c: boolean; }; type IndexedAccess = Example["a"]; type IndexedAccess2 = Example["a" | "b"]; / / number | string type IndexedAccess3 = Example[keyof Example]; / / number | string | boolean type ExAlias = "b" | "c"; type IndexedAccess4 = Example[ExAlias]; / / string | boolean
또한 배열의 요소 타입을 조회하기 위해서 인덱스트 엑세스 타입을 사용하는 경우가 있다.
const PromotionList = [ { type: "product", name: "chicken" }, { type: "product", name: "pizza" }, { type: "card", name: "cheer-up" }, ] ; type ElementOf<T> = typeof T[number]; // type PromotionItemType = { type: string; name: string } type PromotionItemType = ElementOf<PromotionList>;
다른 타입을 기반으로 한 타입을 선언 할 때 사용하는 문법. 인덱스 시그니쳐 문법을 사용해서 반복적인 타입선언을 효과적으로 줄일 수 있다.
type Example = { a: number; b: string; c: boolean; }; type Subset<T> = { [K in keyof T]?: T[K]; } const Example: Subset<Example> = {a: 3 }; const bExample: Subset ‹Example> = { b: "hello" }; const acExample: Subset«xample> = { a: 4, c: true };
맵드 타입을 사용할 때 readonly와 ?를 수식어로 적용 할 수 있다. 이러한 수식어를 더해주는 것 뿐만 아니라 제거할 수도 있다. 기존타입에 readonly와 ? 존재했다면 앞에 -를 붙여 주는 것으로 수식어를 제거한 타입을 선언 가능하다.
type ReadOnlyEx = { readonly a: number; readonly b: string ; }; type CreateMutable<Type> = { -readonly [Property in keyof Type]: Type[Property]; } type ResultType = CreateMutable«ReadOnlyEx>; // { a: number; b : string } type OptionalEx = { a?: number; b?: string; c: boolean; type Concrete<Type> = { [Property in keyof Type]-?: Type[Property]; type ResultType = Concrete‹OptionalEx>; // { a: number; b : string ; c: boolean
덧붙여 맵드 타입에서는 as 키워드를 사용하여 키를 재지정할 수 있다. BottomSheetStore의 키 이름에 BottomSheetMap의 키 이름을 그대로 쓰고 싶은 경우가 있을 수 있고, 모든 키에_BOTTOM_SHEET를 붙이는 식으로 공통된 처리를 적용하 여 새로운 키를 지정하고 싶을 수도 있다. 이럴 때는 아래 예시처럼 as 키워드를 사용해서 효율적으로 처리할 수 있다.
type BottomSheetStore = { [index in BOTTOM SHEET _ID as 'Sindex) _BOTTOM_SHEET']: { resolver?: (payload: any) => void; args?: any; isOpened: boolean; }; };
type Stage = | "init" | "select-image" | "edit-image" | "decorate-card" | "capture-image"; type StageName = `$(Stage)-stage`; // 'init-stage' | 'select-image-stage' | 'edit-image-stage' | 'decorate-card-stage' | 'capture-image-stage'
이렇게 하면 함수, 타입, 클래스 등 여러 타입에 대해 하나하나 따로 정의하지 않아도 되기 때문에 재사용성이 크게 향상된다. 타입 변수는 일반적으로 <T>와 같이 꺾쇠괄호 내부에 정의되며, 사용할 때는 함수에 매개변수를 넣는 것과 유사하게 원하는 타입을 넣어주면 된다. 보통 타입 변수명으로 T(Type), E(Element), K(Key), V(Value) 등 한 글자로 된 이름을 많이 사용한다.
type ExampleArrayType<T> = T[]; const array1: ExampleArrayTypes<string> = ["치킨","피자",우동"] ;
type ExampleArrayType2 = any|]; const array2: ExampleArrayType2 = [ "치킨", { id: 0, name: "치킨", price: 20000, quantity: 1, } 99, true, };
참고로 제네릭 함수를 호출할 때 반드시 꺾쇠괄호(<>) 안에 타입을 명시해야 하는 것은 아니다.
타입을 명시하는 부분을 생략하면 컴파일러가 인수를 보고 타입을 추론해준다.
따라서 타입 추론이 가능한 경우에는 타입 명시를 생략할 수 있다.
function exampleFunc<T>(arg: T): T[] { return new Array(3).fill(arg); } exampleFunc("hello"); // T는 string으로 추론된다
또한 특정 요소 타입을 알 수 없을 때는 제네릭 타입에 기본값을 추가할 수 있다.
interface SubmitEvent<T = HTMLElement>extends SyntheticEvnet<T>{ submitter: T; }
제네릭은 일반화 된 데이터 타입이다. 따라서 함수나 클래스 등의 내부에서 제네릭을 사용할 때 어떤 타입이든 될 수 있다는 개념을 알고 있어야 한다. 특정한 타입에서만 존재하는 멤버를 참조하려고 하면 안된다. 예를 들어 배열에만 존재하는 Length 속성을 제네릭에서 참조하려고 하면 당연히 에러가 발생한다. 컴파일러는 어떤 타입 이 제네릭에 전달될지 알 수 없기 때문에 모든 타입이 Length 속성을 사용할 수는 없다고 알 려주는 것이다.
function exampleFunc2<T>(arg: T): number { return arg.length; // 에러발생: Property 'length' does not exist on type 'T' }
이럴 때는 제네릭 꺾쇠괄호 내부에 'Length 속성을 가진 타입만 받는다'라는 제약을 걸어줌으 로써 Length 속성을 사용할 수 있게끔 만들 수 있다.
interface TypeWithLength { length: number; } function exampleFunc2<T extends TypeWithLength>(arg: T): number { return arg.length; }
제네릭을 사용할 때 주의해야 할 점이 있다. 파일 확장자가 tsx일 때 화살표 함수에 제네릭을 사용하면 에러가 발생한다. tsx는 타입스크립트 + JSX이므로 제네릭의 꺾쇠괄호와 태그의 꺾 쇠괄호를 혼동하여 문제가 생기는 것.
이러한 상황을 피하기 위해서는 제네릭 부분에 extends 키워드를 사용하여 컴파일러에게 특정 타입의 하위 타입만 올 수 있음을 확실히 알려주면 된다. 보통 제네릭을 사용할 때는 function 키워드로 선언하는 경우가 많다.
// 에러발생: JSX element 'T' has no corresponding closing tag const arrowExampleFunc = <T>(arg: T): T[] => { return new Array(3).fill(arg); } // 에러 발생 X const arrowExampleFunc2 = <T extends >(arg: T): T[]=> { return new Array(3).fill(arg); }
어떤 함수의 매개변수나 반환값에 다양한 타입을 넣고 싶을때 제네릭을 사용 할 수 있다. 아래 예시처럼 T 자리에 넣는 타입에 따라 ReadOnlyRepository가 적절히 사용 될 수 있다.
function ReadOnlyRepository<T>(target: ObjectType<T> | EntitySchema<T> | string): Repository<T> { return getConnection("ro").getRepository(target); }
호출 시그니처는 타입스크립트의 함수 타입 문법으로 함수의 매개변수와 반환 타입 을 미리 선언하는 것을 말한다. 호출 시그니처를 사용함으로써 개발자는 함수 호출 시 필요한 타입을 별도로 지정할 수 있게 된다. 호출 시그니처를 사용할 때 제네릭 타입을 어디에 위치시 키는지에 따라 타입의 범위와 제네릭 타입을 언제 구체 타입으로 한정할지를 결정할 수 있다.
interface useSelectPaginationProps<T> { categoryAtom: RecoilState<number>; filterAtom: RecoilState<string[]>; sortAtom: RecoilState<SortType>; fetcherFunc: (props: CommonListRequest) => Promise<DefaultResponse<ContentListRes ponse<T>>>; }
이 코드는 우아한형제들 배민선물하기팀의 호출 시그니처 제네릭 활용 예시다. 여기서 는 useseLectPaginationProps의 타입 별칭으로 한정했다. 따라서 useSelectPagination-Props을 사용할 때 타입을 명시함으로써 제네릭 타입을 구체 타입으로 한정한다. 설명을 덧 붙이면 useSeLectPaginationProps가 사용되는 useSelectPagination 훅의 반환 값도 인 자에서 쓰는 제네릭 타입인 T와 연관 있기 때문에 이처럼 작성했다.
그다음 배민커머스웹프론트개발팀의 활용 예시를 보자.
export type UseRequesterHookType = ‹ RequestData = void, ResponseData = void>( baseURL?: string | Headers, defaultHeader?: Headers ) => [RequestStatus, Requester RequestData, ResponseData>];
이 예시에서 <RequestData, ResponseData>는 호출 시그니처의 일부, 다시 말해 괄호(())
앞에 선언했기 때문에 타입스크립트는 UseRequesterHookType 타입의 함수를 실제 호출할 때 제네릭 타입을 구체 타입으로 한정한다.
배민커머스웹프론트개발팀은 프로젝트 구조를 따르기 위해 아래처럼 작성했다.
function useSelectPagination<T extends CardListContent | CommonProductResponse>({ categoryAtom, filterAtom, sortAtom, fetcherFunc, }: useSelectPaginationProps<T>): { intersectionRef: RefObject<HTMLDivElement>; data: T[]; categoryId: number; isLoading: boolean; isEmpty: boolean; } { // ... return { intersectionRef, data: swappedData ?? [], isLoading, categoryId, isEmpty, }; }
제네릭 클래스는 외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스이다. 제네릭 클래스는 다음과 같은 형태로 선언된다.
class LocalDB<T> { // ... async put(table: string, row: T): Promise<T> { return new Promise<T>((resolved, rejected) => {/* T 타입의 데이터를 DB에 저장 */}); } async get(table:string, key: any): Promise<T> { return new Promise<T>((resolved, rejected)=> {/* T 타입의 데이터를 DB에서 가져옴 */}); } async getTable(table: string): Promise<T[]> { return new Promise<T[]>(resolved, rejected)=> {/* TO 타입의 데이터를 DB에서 가져 옴*/ }); } } export default class IndexedDB implements ICacheStore { private _DB?: LocalDB<{ key: string; value: Promise‹Record<string, unknown>; cacheTTL: number }>; private DB() { if (!this. DB) { this._DB = new LocalDB("localCache", { ver: 6, tables: [{ name: TABLE_NAME, keyPath: "key" }] }); } return this._DB; } // ... }
클래스 이름 뒤에 타입 매개변수인 를 선언해준다. 는 메서드의 매개변수나 반환 타 입으로 사용될 수 있다. LocaLDB 클래스는 외부에서 { key: string; value: Promise<Record<string, unknown>>; cacheTTL: number } 타입을 받아들여 클래스 내부에 서 사용될 제네릭 타입으로 결정된다.
제네릭 클래스를 사용하면 클래스 전체에 걸쳐 타입 매개변수가 적용된다. 특정 메서드만을 대상으로 제네릭을 적용하려면 해당 메서드를 제네릭 메서드로 선언하면 된다.
타입 매개변수에 대한 제약 조건을 설정하는 기능
예를 들어 string 타입으로 제약하려면 타입 매개변수는 특정 타입을 상속해야 한다.
type ErrorRecord<Key extends string>= Exclude<key, ErrorCodeType> extends never ? Partial<Record<Key, boolean>> : never;
이처럼 타입 매개변수가 특정 타입으로 묶였을 때 키를 바운드 타입 매개변수라고 부른다. 그리고 string을 키의 상한 한계라고 한다.
상속 받을수 있는 타입으로 기본 타입뿐만 아니라 상황에 따라 인터페이스나 클래스도 사용 할 수 있다. 또한 유니온 타입을 상속해서 선언 할 수 있다
function useSelectPagination<T extends CardListContent | CommonProductResponse>({ filterAtom, sortAtom, fetcherFunc, }: useSelectPaginationProps<T>): { intersectionRef: RefObject<HTMLDivElement>; data: T[]; categoryld: number; isLoading: boolean; isEmpty: boolean; }{ // ... } // 사용하는 쪽 코드 const { intersectionRef, data, isLoading, isEmpty } = useSelectPagination<CardListContent>({ categoryAtom: replyCardCategoryIdAtom, filterAtom: replyCardFilterAtom, sortAtom: replyCardSortAtom, fetcherFunc: fetchReplyCardListByThemeGroup, });
여러 타입을 상속받을 수 있으며 타입 매개변수를 여러개 둘 수도 있다 .
<Key extends string>
타입을 이런 식으로 제약해버리면 제네릭의 유연성을 잃어버린다. 제네릭의 유연성을 잃지 않 으면서 타입을 제약해야 할 때는 타입 매개변수에 유니온 타입을 상속해서 선언하면 된다.
<Key extends string | number>
유니온 타입으로 T가 여러 타입을 받게 할 수는 있지만, 타입 매개변수가 여러 개일 때는 처리 할 수 없다. 이럴 때는 매개변수를 하나 더 추가하여 선언한다.
다음은 Ok 타입이나 Err 타입을 매개변수 인자로 받아 사용하는 예시이다.
export class APIResponse«Ok, Err = string> { private readonly data: Ok | Err | null; private readonly status: ResponseStatus; private readonly statusCode: number | null; constructor ( data: Ok | Err | null, statusCode: number | null, status: ResponseStatus ) { this.data = data; this.status = status; this.statusCode = statusCode; } public static Success<T, E = string>(data: T): APIResponse<T, E> { return new this<T, E>(data, 200, ResponseStatus.SUCCESS); } public static Error<T, E = unknown>(init: AxiosError): APIResponse<T, E> { if (linit.response) { return new this<T, E>(null, null, ResponseStatus.CLIENT_ERROR); } if (linit.response.data?.result) { return new this<T, E>( null, init.response.status, ResponseStatus.SERVER_ERROR ); } return new this<T, E>( init.response.data.result, init.response.status, ResponseStatus.FAILURE ) } // ... } // 사용하는 쪽 코드 const fetchShopStatus = async (): Promise< APIResponse<IShopResponse | null>> => { // ... return (await API.get<IShopResponse | null>("/v1/main/shop", config)).map((it) => it.result); };
제네릭의 장점은 다양한 타입을 받게 함으로써 코드를 효율적으로 재사용할 수 있는 것이다.
현업에서 가장 많이 제네릭이 활용할 때는 API 응답 값의 타입 을 지정할 때이다.
우아한형제들에서는 API 응답 값의 타입을 지정할 때 제네릭을 활용하여 적절한 타입 추론과 코드의 재사용성을 높이고 있다.
export interface MobileApiResponse<Data> { data: Data; statusCode: string; statusMessage?: string; }
이 코드를 살펴보면 API 응답 값에 따라 달라지는 data를 제네릭 타입 Data로 선언하고 있 다. 이렇게 만든 MobileApiResponse는 실제 API 응답 값의 타입을 지정할 때 아래와 같이 사용되고 있다.
export const fetchPriceInfo = 0: Promise<MobileApiResponse<PriceInfo>> => { const priceUrl = "https: ~"; // url 주소 return request({ method: "GET", url: priceUrl, }); }; export const fetchOrderInfo = (): Promise<MobileApiResponse<Order>> => { const orderUrl = "https: ~"; // url 주소 return request({ method: "GET", url: orderUrl, }); };
이처럼 다양한 API 응답 값의 타입에 MobileApiResponse을 활용해서 코드를 효율적으로 재사용할 수 있다.
이런 식으로 제네릭을 필요한 곳에 사용하면 가독성을 높이고 코드를 효율적으로 작성할 수 있다. 하지만 굳이 필요하지 않은 곳에 제네릭을 사용하면 오히려 독이 되어 코드를 복잡하게 만든다.
제네릭이 필요하지 않을 때도 사용하면 코드 길이만 늘어나고 가독성을 해칠 수 있다. 다음은 제네릭이 굳이 필요하지 않은데도 사용한 예시다.
type GType<T> = T; type RequirementType = "USE" | "UN_USE" | "NON_SELECT"; interface Order { getRequirement(): GType<RequirementType>; }
GType이 다른 곳에서는 사용되지 않고 getRequirement 함수의 반환 값 타입으로만 사용되 고 있다고 가정해보자.
GType이라는 이름이 현재 사용되고 있는 목적의 의미를 정확히 담고 있지도 않을뿐더러 굳이 제네릭을 사용하지 않고 타입 매개변수를 그대로 선언하는 것과 같은 기능을 하고 있다.
즉, 아래처럼 사용하는 것과 동일하다.
type RequirementType = "USE" | "UN_USE" | "NON_SELECT"; interface Order { getRequirement(): RequirementType; }
제네릭은 코드의 재사용성을 높이고 타입 추론을 하는 데 사용된다. 그러나 any를 사용하면 제네릭의 장점과 타입 추론 및 타입 검사를 할 수 있는 이점을 누릴 수 없게 된다. any 타입은 모든 타입을 허용하기 때문에 사실상 자바스크립트와 동일한 방식으로 코드를 작성하는 것과 같다. 따라서 any를 사용하면 제네릭을 포함해 타입을 지정하는 의미가 사라지게 된다.
type ReturnType<T = any> = { // ... };
제네릭이 과하게 사용되면 가독성을 해치기 때문에 코드를 읽고 타입을 이해하기가 어려워진다. 부득이한 상황을 제외하고 복잡한 제네릭은 의미 단위로 분할해서 사용하는 게 좋다.
ReturnType<Record<OrderType,Partial<Record<CommonOrderStatus | CommonReturnStatus, Partial<Record<OrderRoleType, string[]>>>>>>>; type CommonStatus = CommonOrderStatus | CommonReturnStatus; type PartialOrderRole = Partial<Record<OrderRoleType, string[]>>; type RecordCommonOrder = Record<CommonStatus, PartialOrderRole>; type RecordOrder = Record<OrderType, Partial<RecordCommonOrder>>; ReturnType‹RecordOrder>;
만약에 내가 작성한 코드를 다른 개발자가 쉽게 이해하지 못하고 있다면 혹시 제네릭을 오남 용하고 있는 것은 아닌지 검토해봐야 한다.