FrontEnd

[이펙티브 타입스크립트] 4장 타입 설계

mingg123 2023. 6. 30. 18:34

아이템 28  유효한 상태만 표현하는 타입을 지향하기

  • 타입을 잘 설계하려면 유효한 상태만 표현할 수 있는 타입을 만들어야함 

 

타입 설계가 잘못 된 예시 1

  interface State {
    pageText: string;
    isLoading: boolean;
    error?: string;
  }

  function renderPage(state:State) {
    if(state.error) {
        return `Error`;
    } else if(state.isLoading) {
        return 'loading currentPage';
    }
    return 'currentPage'
  }

 

isLoading이 true이고 동시에 error 값이 존재한다면 로딩 중인 상태인지, 오류가 발생한 상태인지 명확하게 구분이 힘듬. 

 

async function changePage(state: State, newPage: string) { 
    state.isLoading = true;
    try {
        const response = await fetch(getUrlForPage(newPage)); 
        
        if (!response.ok) {
        	throw new Error('Unable to load ${newPage}: ${response.statusText}'); 
        }
        
        const text = await response.text(); 
        state.isLoading = false; 
        state.pageText = text;
    } 
    catch (e) { 
        state.error = '' + e;
    } 
}

문제점

  • try문 내에 에러가 발생했을 경우 state.isLoading을 false하는 부분이 없음 
  • state.error를 초기화 하지 않았기 때문에, 페이지 전환중에 과거 로딩 에러 메세지를 보기 있게 됨

 

수정한 타입 설계 예시

interface RequestError { 
    state: 'error';
    error: string;
}

interface Requestsuccess {
    state: 'ok';
    pageText: string; 
}

type Requeststate = RequestPending | RequestError | Requestsuccess;

interface State {
    currentPage: string;
    requests: {[page: string]: Requeststate};
}

pending, error, success 네트워크 요청 상태를 태그된 유니온방식으로 사용함. 

 

장점

  • 무효한 상태를 허용하지 않음

 

단점

  • 코드의 길이가 길어짐

 

수정된 타입설계를 바탕으로 수정한 함수

function renderPage(state: State) {
    const {currentPage} = state;
    const requeststate = state.requests[currentPage]; 
    
    switch (requeststate.state) {
        case 'pending':
        return 'Loading ${currentPage}...';
        case 'error':
        return 'Error! Unable to load ${currentPage}: ${requeststate.error}';
        case 'ok':
        return '<hl>${currentPage}</hl>\n${requeststate.pageText}';
    }
}



async function changePage(state: State, newPage: string) { 
    state.requests[newPage] = {state: 'pending'}; 
    state.currentPage = newPage;
    
    try {
    	const response = await fetch(getUrlForPage(newPage)); 
    
        if (!response.ok) {
        	throw new Error('Unable to load ${newPage}: ${response.statusText}'); 
        }
    
    	const pageText = await response.text();
    	state.requests[newPage] = {state: 'ok', pageText}; 
    
    } catch (e) {
    	state.requests[newPage] = {state: 'error', error: '' + e}; 
    }
}
  • requeststate에 대한 모호함이 사라짐

 

타입 설계가 잘못 된 예시 2

기장과 부기장이 조종하는 스틱 

interface CockpitControlers {
    leftSideStick: number;
    rightSideStick: number;
}

 

스틱의 설정을 계산하는 함수

왼쪽 스틱이 중립이면 오른쪽 스틱을 이용하고, 오른쪽 스틱이 중립이면 왼쪽 스틱을 이용해야한다함. 

function getStickSetting(controls: CockpitControls) { 
    const {leftSideStick, rightSideStick} = controls;

    if (leftSideStick === 0) {
        return rightSideStick; 
    }
    return leftSideStick; 
}

두 스틱이 모두 중립이 아닌 경우에는 ?

두 스틱이 비슷한 값이면 스틱의 각도를 평균해서 계산한다함

function getStickSetting(controts: CockpitControls) { 
    const {leftSideStick, rightSideStick} = controls;

    if (leftSideStick === 0) {
    	return rightSideStick;
    } else if (rightSideStick === 0) {
    	return leftSideStick; 
    }
    if (Math.abs(leftSideStick - rightSideStick) < 5) { 
    	return (leftSideStick + rightSideStick) / 2;
    }
    // ??? 
}

스틱의 각도가 큰 경우에는 해결이 어렵다함.

 

기장은 스틱을 앞으로 당기고있고, 부기장은 스틱을 뒤로 당기고 있다면 ?

function getStickSetting(controls: CockpitControls) {
	return (controls.leftSideStick + controls.rightSideStick) / 2;
}

평균값에는 변함이 없었고 비행기는 하강하지 않아서 추락했다.

 

 

 

수정된 타입 설계 예시

(가정)

대부분의 비행기는 두 개의 스틱이 기계적으로 연결되어있다.

기장이 뒤로 당기면, 부기장의 스틱도 뒤로 당겨진다.

interface CockpitControls {
/** 스틱의 각도, 0 = 중립, + = 앞으로 **/
	stickAngle: number;
}
  • 스틱의 설정을 계산하는 함수 getStickSetting 함수가 필요없어짐

 

요약

  • 유효한 상태와 무효한 상태를 둘 다 표현하는 타입은 혼란을 초래하기 쉽고 오류를 유발함
  • 유효한 상태만을 표현하는 타입을 지향해야함 
  • 코드가 길어지고 표현하기 어렵지만, 결국 시간을 절약하고 고통을 줄여줌

 

 

 

아이템 29 사용할 때는 너그럽게, 생성할 때는 엄격하게

 

  • 함수의 매개변수 타입은 범위가 넓어도 되지만, 반환 타입은 범위가 구체적이여야 함 

타입 설계가 아쉬운 예시

declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): CameraOptions;


interface CameraOptions { 
    center?: LngLat;
    zoom?: number;
    bearing?: number; pitch?: number;
}

type LngLat =
{ lng: number; lat: number; } | { lon: number; lat: number; } |
[number, number];

type LngLatBounds =
{northeast: LngLat, southwest: LngLat} |
[LngLat, LngLat] |
[number, number, number, number];

 

타입을 사용한 함수 예시

function focusOnFeature(f: Feature) {
    const bounds = calculateBoundingBox(f); 
    const camera = viewportForBounds(bounds); 
    setCamera(camera);
    const {center: {lat, lng}, zoom} = camera;
    // ~ ... 형식에 'lat' 속성이 없습니다.
    // ~ 형식에 '*Ing 속성이 없습니다. 
    zoom; // 타입o| number | undefined
    window.location.search = ' ?v=@${lat},${lng}z${zoom}'; 
}
  • type LngLat에 {lat, lng} 타입은 없음으로 속성이 없다로 오류남
  • zoom도 CameraOption에 zoom?임으로 number | undefined 오류남
  • camera의 타입(viewportForBounds의 타입 선언)이 사용될때 뿐만아니라 만들어질때에도 너무 자유로워서 문제임

 

해결법

  • camera 타입을 유니온 타입으로 만듬 
  • viewportForBounds의 반환 타입을 엄격하게 만듬
  • 사용하기 편리한 API일 수록 반환 타입이 엄격함

 

수정된 타입 설계 예시

interface LngLat { lng: number; lat: number; };

type LngLatLike = LngLat | { lon: number; lat: number; } |
[number, number];

interface Camera { 
    center: LngLat; 
    zoom: number; 
    bearing: number; 
    pitch: number;
}

interface CameraOptions extends Omit<Partial<Camera>, 'center'> {
center?: LngLatLike; }
type LngLatBounds =
{northeast: LngLatLike, southwest: LngLatLike} |
[LngLatLike, LngLatLike] | [number, number, number, number];

declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): Camera;
  • 엄격하 Camera 타입을 느슨한 CameroOptions 타입으로 변환 
  • Partial로 Camera 타입을 ? 로 선택적으로 바꾸고, Omit으로 center 타입을 제거한 이후 center?: LngLatLike 타입을 추가함

Omit과 Partial이 복잡해 보인다면 아래와 같이 CameraOptions를 사용해도 됨

interface CameraOptions { 
    center?: LngLatLike; 
    zoom?: number;
    bearing?: number; 
    pitch?: number;
}

 

타입을 사용한 함수 예시

function focusOnFeature(f: Feature) {
const bounds = calculateBoundingBox(f);
const camera = viewportForBounds(bounds); setCamera(camera);
const {center: {lat, lng}, zoom} = camera; // 정상
zoom; // 타입이 number window, location.search = '?v=@${lat};${lng}z${zooni}';
}
  • lat, lng가 있음으로 타입 오류가 나지 않음
  • camera의 타입의 zoom이 number임으로 number | undefined 오류가 나지 않음

요약

  • 선택적 속성과 유니온 타입은 매개변수 타입에서 사용하자
  • 반환타입은 최대한 엄격하게 사용하자
  • 매개변수와 반환 타입의 재사용을 위해선 기본 형태와 Partial를 이용하여 느슨한 타입을 도입하는 방법도 괜찮다.

 

아이템  30 문서에 타입 정보를 쓰지 않기

 

지양해야 하는 예시

/* 
매개변수가 있을 때는 특정 페이지의 전경색을 반환합니다.
*/
function getForegroundColor(page?: string) {
return page === 'login' ? {r: 127, g: 127, b: 127} : {r: 0, g: 0, b: 0}; }

문제점

  • 코드의 정보와 주석의 정보가 일치 하지 않음
  • 주석에는 전경색을 반환한다고 했지만 코드는 {r,g,b} 객체를 반환함
  • 주석이 불필요하게 장황함. 

수정한 예시

/** 애플리케이션 또는 특정 페이지의 전경색을 가져옵니다. **/
function getForegroundColor(page?: string): Color { 
// ...
}

 

요약

  • 주석과 변수명에 타입 정보를 작성하는 것을 지양하자
  • 타입이 명확하지 않는 경우에는 변수명에 단위 정보를 포함하는 것을 고려해보자
    • ex) timeMS or temperatureC

 

아이템  31 타입 주변에 null 값 배치하기

 

  • 변수 B가 변수 A의 값으로 부터 비롯되는 값이라면 A가 null이냐 null이 아니냐에따라 B의 값도 null이 될 수도 있다.

 

strictNullCheck 비활성화의 경우 

function extent(nums: number[]) { 
	let min, max;
    
    for (const num of nums) {
        if (!min) { 
        	min = num;
            max = num;
        } else {
            min = Math.min(min, num); 
            max = Math.max(max, num);
    	} 
     }
	return [min, max]; 
}
  • 타입 체커를 통과함
  • 최솟값이나 최대값이 0인 경우 [0,1,2] 의 경우 [0,2]가 아닌 [1,2] 결과가 나옴

 

strictNullCheck 활성화의 경우

function extent(nums: number[]) { 
	let min, max;
    
    for (const num of nums) {
        if (!min) { 
        	min = num; 
            max = num;
        } else {
            min = Math.min(min, num); 
            max = Math.max(max, num); // 'number | undefined' 형식의 인수는 'number' 형식의 매개변수에 할당될 수 없다.
    	} 
     }
	return [min, max]; 
}


// 사용시
const [min, max] = extent([0,1,2]);
const span = max - min; // ~~개체가 'undefined' 인 것 같습니다
  • 함수내에서도 에러가 보임
  • 사용시에도 undefined 에러가 보임 

 

수정한 함수 

function extent(nums: number[]) {
    let result: [number, number] | null = null; 
    for (const num of nums) {
        if (!result) {
        	result = [num, num];
        } else {
        	result = [Math.min(num, result[0]), Math.max(num, result[1])];
        } 
    }
return result; 
}

// 사용시
const [min, max] = extent([0,1,2]);
const span = max - min
  • result라는 객체를 만들어서 설계를 개선함
  • null 관련된 버그를 제거함 
  • 사용시에도 타입 에러를 제거함 

 

null과 null 이 아닌 값을 섞여서 사용한 경우 문제가 발생한 예시

  class UserPosts {
    user: UserInfo | null;
    posts: Post[] | null;

    constructor() {
      this.user = null;
      this.posts = null;
    }

    async init(userId: string) {
      return Promise.all([
        async() => this.user = await fetchUser(userId),
        async() => this.posts = await fetchPostsForUser(userId)
      ]);
    }

    getUserName() {
      
    }
  }

문제점

  • 네트워크 요청이 오는 동안 user, posts 둘다 null 상태임
  • 시점마다 둘 다 null 이거나, 둘 중 하나만 null 이거나, 둘 다 null 이 아닌 경우가 등 4가지 경우의 수가 존재함
  • null 체크를 하지 않으면 버그가 발생할 수 있음 

 

수정한 코드

  class UserPosts {
    user: UserInfo;
    posts: Post;

    constructor(user: UserInfo, posts: Post[]) {
      this.user = user;
      this.posts = posts;
    }

    static async init(userId: string) {
      return Promise.all([
        async() => this.user = await fetchUser(userId),
        async() => this.posts = await fetchPostsForUser(userId)
      ]);
      return new UserPosts(user, posts);
    }

    getUserName() {
      return this.user.name;
    }
  }
  • user, posts 의 타입이 null을 허용하지 않음
  • 생성자에서 부터 값을 주입해줌 (UserPosts 클래스는 완전히 null 이 아니게 됨)

 

요약

  • 한 값의 null 여부가 다른 값의 null 여부에 관련되도록 설계하면 안됨
  • API 작성시에는 반환 타입을 전체가 null이거나, null 이 아니게 설계 해야함
  • 클래스를 만들 시에는 필요한 모든 값이 준비되었을 때 생성해야함(null 존재 X)
  • null 체크를 위해선 strictNullChecks를 설정해야함

 

아이템 32 유니온의 인터페이스보다는 인터페이스의 유니온을 사용하기

 

아이템32를 읽기전에 유니온 인터페이스와 인터페이스 유니온의 정의가 너무 헷갈려서 정리를 하려고 한다.

 

유니온 인터페이스

 

  • 여러 개의 인터페이스를 하나의 새로운 인터페이스로 결합하는 방식
  • 여러 인터페이스의 속성을 하나의 인터페이스로 합쳐서 사용할 수 있다. 
  • 하나의 인터페이스 안에서 값의 타입으로 유니온을 사용하는 것

 

 

인터페이스 유니온

 

  • 유니온 타입을 이용하여 인터페이스를 다양한 타입으로 결합하는 방식. 
  • 인터페이스가 여러 타입을 허용하도록 하는 것

 

 

유니온 인터페이스

interface Layer {
  layout: FillLayout | LineLayout | PointLayout;
  paint: FillPaint | LinePaint | PointPaint;
}

 

유니온 인터페이스로 작성을 하게되면 layout FillLayout이면서 paint LinePaint인 경우가 발생한다.

그렇게이 인터페이스 유니온 혹은 태그된 유니온 으로 작성하는게 좋음

 

인터페이스 유니온

interface FillLayer {
  layout: FillLayout;
  paint: FillPaint;
}

interface LineLayer {
  layout: LineLayout;
  paint: LinePaint;
}

interface PointLayer {
  layout: PointLayout;
  paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;

두개 차이점은 알겠는데 용어가 왜이리 헷갈리냐.. 잘 기억할 수 있는 팁 아시면 댓글남겨주세요.

 

태그된 유니온

interface FillLayer {
  type: 'fill';
  layout: FillLayout;
  paint: FillPaint;
}

interface LineLayer {
  type: 'line';
  layout: LineLayout;
  paint: LinePaint;
}

interface PointLayer {
  type: 'paint';
  layout: PointLayout;
  paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;

type 속성은 '태그' 이며 런타임에서 Layer가 어떤 타입인지 판단하는데 도움이 된다. 

 

 

여러개의 선택적 필드가 있는 경우 예시

birth내 place, date가 둘 다 없거나 둘 다 있을 경우

 

지양 하는 방식

  interface Person {
    name: string;
    placeOfBirth?: string;
    dateOfBirth?: Date;
  }

 

지향 하는 방식

 interface Person {
    name: string;
    birth?: {
      place: string;
      date: Date;
    }
  }

두개의 속성을 하나의 객체로 모으는 것이 더 나은 설계임. 

 

 

만약 타입의 구조를 손 댈 수 없는 상황(API의 결과)라면 인터페이스의 유니온을 사용하면 됨. 

interface Name { 
  name: string;
}

interface PersonWithBirth extends Name {
  placeOfBirth: string;
  dateOfBirth: Date;
}

type Person = Name | PersonWithBirth;

function test(p : Person) {
  if('placeOfBirth' in p) {
    p
    const {dateOfBirth} = p
  }
}

 

요약

 

  • 유니온 타입의 속성을 여러 개 가지는 인터페이스는 속성 간의 관계가 분명하지 않기 때문에 실수가 발생할 수 있다.
  • 유니온 인터페이스보단 인터페이스 유니온이 더 정확하고 타입스크립트가 이해하기 좋다.
  • 타입스크립트가 제어 흐름을 분석할 수 있도록 태그를 넣는 것을 고려해보자. 자주 사용하는 패턴이다.(예시 Layer)

 

 

아이템 33  string 타입보다 더 구체적인 타입 사용하기

 

지양하는 방식

interface Album {
  artist: string;
  title: string;
  releaseDate: string;
  recodingType: string; // live or studio
}

const kindOfBlut: Album = {
  artist: 'Miles Davis',
  title: 'Kind of Blue',
  releaseDate: 'August 17th',
  recodingType: 'Studio'
}
  • string의 타입범위는 매우 넓기 때문에, recodingType 'studio'를 예상했지만 'Studio'가 들어와도 방어하지 못한다.
  • 매개변수의 순서가 잘못된 경우에도 잡아낼 수가 없다. 

 

지향하는 방식

interface Album {
  artist: string;
  title: string;
  releaseDate: Date;
  recodingType: RecodingType; // live or studio
}

const kindOfBlut: Album = {
  artist: 'Miles Davis',
  title: 'Kind of Blue',
  releaseDate: new Date('1995-08-17')
  recodingType: 'Studio' //Type '"Studio"' is not assignable to type 'RecodingType'. Did you mean '"studio"'?
}

 

타입을 명시적으로 정의함으로써 이점 

  • 다른 곳으로 값이 전달되어도 타입 정보가 유지됨 
  • 타입의 의미를 설명하는 주석을 넣을 수 있음 
  • keyof 연산자로 더욱 세밀하게 객체의 속성 체크가 가능함 

 

keyof 연산자 사용 예시

// 지양해야 하는 방식 

function pluck(records: any[], key: string): any[] {
  return records.map(r => r[key]);
}

// 타입 에러 발생 
function pluck<T>(records: T[], key: string) : any[]{
  return records.map(r => r[key]);  // Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'unknown'.
}

// keyof를 이용한 해결
function pluck<T>(records: T[], key: keyof T) {
  return records.map(r => r[key]);
}

 

허나 key값으로 releaseDate를 넣었을 경우 Date[]가 아닌,  (string | Date)[]로 타입의 범위가 넓게 나온다. 

 

 

조금 더 타입을 좁히기 위해 extneds를 사용한다.

function pluck<T, K extends keyof T>(records: T[], key: K): T[K][] {
  return records.map(r => r[key]);
}

 

타입 시그니처가 완벽해졌다. 

 

요약

 

  • string 타입을 남발하지 말자
  • 변수의 범위를 정확하게 표현하고 싶다면 string 보다는 문자열 리터럴 타입을 이용하자. 
  • 객체의 속성을 받을 경우에는 string 보단 keyof를 이용하자 

 

아이템 34  부정확한 타입보다는 미완성 타입을 사용하기

보통 타입 설계를 구체적으로 할수록, 버그를 많이 잡을 수 있다. 

허나 실수가 발생하기 쉽고 잘못된 타입은 차라리 타입이 없는 것보다 못할 수 있다.

 

지양해야 하는 방식

type Expression1 = any;
type Expression2 = number | string | any[];

const tests: Expression2[] = [
  10,
  "red",
  true, // 타입 에러 발생
  ["+", 10, 5],
  ["case", [">", 20, 10], "red", "blue"], 
  ["**", 2, 31],
  ["rgb", 255, 0, 127, 0]
]

타입에러가 발생하여 아래에 좀 더 구체적인 타입을 추가함

interface MathCall {
  0: '+' | '-' | '/' | '*';
  1: Expression4;
  2: Expression4;
}

interface CaseCall {
  0: 'case';
  1: Expression4;
  2: Expression4;
  3: Expression4;
}

interface RGBCall {
  0: 'rgb';
  1: Expression4;
  2: Expression4;
  3: Expression4;
}

type Expression4 = number | string | CallExpression;
type CallExpression = MathCall | CaseCall | RGBCall;

타입의 정보는 정밀해졌지만 이전 버전보다 개선되었다고 보기가 어려움. 

오류 메세지는 더 난해해짐. 

어설프게 타입을 선언하다간 오히려 역효과가 발생함 

 

요약

  • 타입 선언을 정확하게 할 수 없다면 하지 말아야함. 
  • 타입 정보를 구체적으로 설계할 수록 오류 메세지와 자동 완성 기능에 주의를 기울여야함 

 

 

아이템 35  데이터가 아닌, API와 명세를 보고 타입 만들기 

타입은 잘 설계하면 이점이 많지만, 잘못된 설계는 오히려 악효과를 낼 수 있다. 이로인해 타입을 자동으로 생성한다면 유용할 것이다. 

가능한 파일 형식, API, 명세 등 데이터를 보고 판단하는 것이 아닌 명세서를 보고 타입을 생성해야한다. 

 

 

아이템 36  해당 분야의 용어로 타입 이름 짓기

타입의 이름을 명확하게 지어야 코드의 의도를 파악하기 쉽다.

 

지양 해야하는 방식

interface Animal {
  name: string;
  endangered: boolean;
  habitat: string;
}

const leopard: Animal = {
  name: 'Snow Leopard',
  endangered: false,
  habitat: 'tundra'
}
  • name이 동물의 학명인지 명칭인지 불분명함
  • endangered속성이 멸종위기를 표현하기 위한 것인데 멸종된 동물이 true인지 false인지 파악이 어려움
  • habitat 서식지가 string으로 넓은 타입임

 

지향 해야하는 방식

interface Animal {
  commonName: string;
  genus: string;
  species: string;
  status: ConservationStatus;
  climates: KoppenClimate[];
}

type ConservationStatus = 'EX' | 'EW' | 'VU'
type KoppenClimate = 'Af' | 'Am' | 'As' | 'EF'

const snowLeopard: Animal = {
  commonName: 'Snow Leopard',
  genus: 'Panthera',
  species: 'Uncia',
  status: 'VU',
  climates: ['Af', 'EF']
}
  • name을 commonName, genus, species로 좀 더 구체적인 용어로 대체함
  • endangered는 보호등급에 대한 체제인 ConservationStatus로 변경
  • habitat은 기후를 의미하는 climates로 변경

 

  • 코드로 표현하고자 하는 분야의 용어를 내가 만들지말고 오래전부터 존재해왔던 전문적인 용어를 이용하자. 
  • data, info, thing, item, object, entity같은 모호한 이름은 피하자(찔리네)
  • 이름을 지을때는 포함된 내용이나 계산방식보다는 데이터 자체가 무엇인지 고려해야함

 

아이템 37  공식 명칭에는 상표를 붙이기 

구조적 타이핑으로 인해 가끔 코드가 의도치 않은 결과를 낼 수 있다. 

 

예시

interface Vector2D {
  x: number;
  y: number;
}

function calculateNorm(p: Vector2D) {
  return Math.sqrt(p.x * p.x + p.y * p.y);
}

calculateNorm({x:3, y:4}); // 정상, 5를 반환 
const vec3D = {x:3, y:4, z:1}; // 정상! 결과는 동일하게 5
calculateNorm(vec3D);

vec3D를 살펴보면 구조적 타이핑 관점에서는 문제가 없지만, 2차원 백터를 사용해야 하는 것이 맞다.

3차원 백터를 허용하지 않게 하려면 상표(brand)를 붙인다.

 

 

수정한 예시

interface Vector2D {
  _brand: '2d';
  x: number;
  y: number;
}

function v2c2D(x: number, y: number): Vector2D {
  return {x,y, _brand: '2d'};
}

function calculateNorm(p: Vector2D) {
  return Math.sqrt(p.x * p.x + p.y * p.y);
}

calculateNorm({x:3, y:4}); // 정상, 5를 반환 
const vec3D = {x:3, y:4, z:1}; 
calculateNorm(vec3D); // '_brand' 속성이 없습니다

단순한 실수를 방지할 수 있음. 

절대 경로를 이용한 예시를 추가로 들고있는데 상표 기법을 실제로 많이 쓰는지 모르겠음