Typescript 유틸리티 타입(Partial, Pick, Omit, Record, Readonly)

공통 타입 변환을 위한 유틸리티 타입

타입스크립트에서 제공하는 유틸리티 타입(Utility Types)은 type, interface를 입맛에 맞게 사용할 수 있도록 도와주는 유용한 기능이다.

유틸리티 타입의 종류는 다음 그림과 같이 매우 많다(타입스크립트 공식 페이지).

이 중 사용 빈도가 잦은 Partial, Pick, Omit, Record, Readonly에 대해 알아보자.

1. Partial

partial은 ‘부분적인’이라는 뜻으로, 정의된 타입의 속성을 모두 옵셔널로 사용하여 부분적으로 원하는 속성만 사용하도록 할 수 있다.

우선 Partial의 정의는 다음과 같다.

type Partial<T> = {
  [P in keyof T]?: T[P];
};

Partial<Type>의 방식을 통해 속성을 옵셔널하게 사용하며, 예시는 다음과 같다.

 
interface Hardware{
    cpu: string;
    gpu: string;
    memory: number;
    storage: number;
    accessary: string;
}

const selectOptions = (default: Hardware, selected: Partial<Hardware>) => {
    setOptions({...default, ...selected});
};

const defaultOption = {
    cpu: "M1";
    gpu: "10-core";
    memory: 16;
    storage: 512;
    accessary: "Touch ID";
}

const selectedOption = {
    cpu: "M2";
    memory: "32";
    storage: "1024";
}

이와 같은 방법으로 사용하지 않는 속성을 옵셔널로 변경하여 사용할 수 있다.

2. Pick

pick은 ‘뽑다’는 뜻으로, 정의된 타입에서 속성을 지정하여 사용할 수 있다.

Pick의 정의는 다음과 같다.

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

Pick<Type, Keys>의 방식을 사용해 사용할 속성을 문자열이나 유니온 타입으로 Keys에 지정하여 해당 속성만 사용한다.

interface Exercise{
    title: string;
    type: string;
    time: number;
    complete: boolean;
}

type BriefExercise = Pick<Exercise, "title" | "complete">;

const today = {
    title: "jogging";
    complete: true;
}

Partial은 모든 속성을 옵셔널로 사용하지만 Pick은 속성을 지정하여 필요한 타입만 정의하는 방식으로 사용한다.

3. Omit

omit은 ‘생략하다’라는 뜻으로, 정의된 타입에서 생략할 속성을 지정하여 사용할 수 있다.

Omit의 정의는 다음과 같다.

type Omit<T, K extends keyof any> = {
  [P in Exclude<keyof T, K>]: T[P]
}

Omit<Type, Keys>의 방식으로 사용하며, Pick과 반대로 필요하지 않은 타입을 지정하여 정의하는 방식으로 사용한다.

interface Exercise{
    title: string;
    type: string;
    time: number;
    complete: boolean;
}

type BriefExercise = Omit<Exercise, "type" | "time">;

const today = {
    title: "jogging";
    complete: true;
}

Pick과 Omit의 용도는 같지만 정의 방법은 상반된다.

정의된 타입에서 생략하고 싶은 속성이 많을 때는 Pick, 생략하고 싶은 속성이 적을 때는 Omit을 사용하면 효과적이다.

4. Record

record는 ‘기록하다’는 뜻으로, key-value 세트를 생성하여 사용할 수 있다.

Record의 정의는 다음과 같다.

type Record<K extends string, T> = {
  [P in K]: T;
};

Record<Keys, Type>의 방식으로 사용하며, 두 개의 타입에서 한 타입을 Keys, 한 타입을 Type으로 조합하여 사용할 수 있다.

type MacBookType = "air13" | "air15" | "pro13" | "pro14" | "pro16";

interface Performance{
    cpu: string;
    memory: number;
    storage: number;
};

const OnTheMarket: Record<MacBookType, Performance> = {
    air13: { cpu:"M1", memory:8, storage: 256 };
    air15: { cpu:"M2", memory:16, storage: 512 };
    pro13: { cpu:"M2", memory:16, storage: 1024 };
    pro14: { cpu:"M2 pro", memory:32, storage: 1024 };
    pro16: { cpu:"M2 ultra", memory:96, storage: 2048 };
}

위와 같은 방식으로 두 타입을 조합해서 사용할 수 있다.

5. Readonly

readonly는 ‘읽기 전용’이므로 타입을 읽기 전용으로 정의하여 사용할 수 있다.

Readonly의 정의는 다음과 같다.

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

타입을 사용해 readonly 속성을 부여하고 싶은 경우에 사용할 수 있다.

interface LimitSize{
    size: number;
}

const myData: Readonly<LimitSize> = {
    size: 32;
}

myData.size = 64; // error

CSS의 em, rem, vh, vw, %의 단위를 알아보자

반응형을 위한 단위의 활용

CSS에서 가장 기본이 되는 크기 단위가 픽셀(px)인데요.

1px은 0.00264cm로 환산되는 길이로, 300px은 약 8cm가 됩니다.

16px은 0.42cm인데요. 이 16px이 바로 CSS에서 기본값입니다.

font-size : 16px은 세로 길이가 16px(0.423cm)인 폰트 사이즈라는 의미로, 출판물에서 기본적으로 사용되는 사이즈입니다.

그럼 이 기본 사이즈 16px을 기반으로 em, rem에 대해 알아보겠습니다.

em

em은 기준 크기에 대한 배수를 의미합니다.

기준은 font-size이며 같은 요소의 사이즈에서 시작해 부모로 차근차근 올라갑니다.

예를 들어 같은 요소에 font-size:16px이 설정된 경우 em의 기준은 16px이 됩니다.

.child-size{
    font-size : 16px;
    width : 2em;   // 2 * 16px = 32px;
}

위 코드에서 child-size 내부에 font-size가 있으므로 해당 요소의 width : 2em는 16의 2배인 32px이 됩니다.

만약 다음과 같이 같이 child-size 내부에 font-size가 정의되어 있지 않으면 해당 요소를 감싸고 있는 부모 클래스에서 기준 사이즈를 가져오게 됩니다.

.parent-size{
    font-size : 20px;
}
.child-size{
    width : 2em;   // 2 * 20px = 40px;
}

최상단까지 이동해도 font-size가 없는 경우에는 위에서 말한 기본값인 16px을 기준으로 지정합니다.

rem

rem은 em과 비슷하지만 r은 root를 의미합니다.

따라서 최상단인 html에 정의된 값을 기준으로 사용합니다.

html{
    font-size : 20px;
}
.child-size{
    font-size : 16px;
    width : 2rem;   // 2 * 20px = 40px;
}

최상단의 크기를 기준하므로 20px이 기준이 되어 2rem은 40px로 계산됩니다.

vw

Viewport Width는 스크롤을 포함한 윈도우의 너비를 의미하며, 반응형을 위해 상대적 크기를 사용할 수 있습니다.

1vw는 뷰포트 너비의 1%에 해당하는 값으로 너비가 1000px인 경우 1vw는 10px, 100vw는 1000px이 됩니다.

브라우저의 사이즈 변경이나 모바일 등에서 전체 비율을 맞추고 싶을 때 효과적으로 사용할 수 있으며, 화면을 가로로 가득 채우고 싶을 때 100vw를 사용할 수 있습니다.

.layout{
    width : 100vw;
}

vh

Viewport Height는 윈도우의 높이를 의미하며 vw와 마찬가지로 뷰포트 높이의 1%를 의미합니다.

화면을 가득 채우고 싶을 때는 다음과 같이 너비와 높이를 지정하면 됩니다.

.layout{
    width : 100vw;
    height: 100vh;
}

%

%는 부모 요소에 따라 크기가 결정됩니다.

높이를 100%로 설정하는 경우는 부모의 높이의 100%를 설정하게 됩니다.

따라서 부모의 높이가 100px인 경우 자식 요소에서 height : 100%를 설정하면 높이는 50가 됩니다.

width : 100%인 경우 스크롤바의 너비를 생략한 크기를 나타냅니다.

font-size : 100%의 경우 부모 요소의 사이즈를 기준으로 사용하게 되므로 기본값은 16px이 됩니다.

CSS selector 우선순위, 명시도(specificity)

요소 선택자(Element Selector) 점수 계산하기

CSS에서는 요소의 선택자에 따라 우선순위가 나뉜다.

우선순위 가중치를 통해 스타일을 적용할 순서를 결정하지만 현재는 가중치 계산 방식이 브라우저별로 다르기 때문에 선택자의 상대 우선순위를 알아두는 것이 더 좋다.

예를 들어 기존에는 가중치가 class:10, id:100으로, class 10개가 1개의 id와 동등한 가중치를 가졌지만 현재는 가중치가 달라져 일부 브라우저에서는 256개의 class가 1개의 id와 동등한 우선 순위를 가진다. (참고:stackoverflow)

선택자는 다음과 같은 우선순위를 가진다.

!important > inline style > id > class (pseud0) > element (pseudo)

따라서 선택자별로 상대적인 가중치를 판단하여 높은 순서에 따라 왼쪽부터 comma로 구분하여 계산하는 것이 좋다.

높음inline style , id , class (pseud0) , element (pseudo) <낮음
Ex1) 0 , 1 , 2 , 1
Ex2) 0 , 0 , 200 , 10
👉 Ex1이 Ex2보다 가중치가 높음

예를 들어 다음과 같이 id, class 선택자가 모두 사용되는 경우 우선순위에 따라id 스타일이 적용된다.

.myMenuClass{
  color:green;
}

#myMenuId{
  color:red;
}

<h1 id="myMenuId" class="myMenuClass">This is it</h1>

그리고 가중치의 합산은 다음과 같다.

#myMenuId{
  color:red;
}

.myMenuClass{
  color:blue;  
}

h1#myMenuId{
  color:orange;  
}

<h1 id="myMenuId" class="myMenuClass">This is it!</h1>

우선 순위를 계산해보면,

#myMenuId : 0,1,0,0 / .myMenuClass : 0,0,1,0 / h1#myMenuId : 0,1,0,1

따라서 마지막 orange color가 적용된다.

!important는 모든 가중치를 무시하고 최우선적으로 적용하는 치트키와 같다. !important가 요소에 중복되는 경우 가장 뒤에 선언된 !important가 적용된다.

하지만 !important의 사용&남용은 평화로운 생태계를 위협하는 존재가 될 수 있으므로 최대한 사용하지 않는 것이 정신 건강과 코드 구성에도 좋다.

마지막으로 예외 사항도 알아보자.

우선순위 없음: *,:where()
가중치 포함하지 않음: >, ~, +
가장 높은 가중치 하나만 선택: :is(), :not()
ex) :is(#menuId, .menuId, span) -> 가중치 가장 높은 #menuId만 카운트

맥에서 localhost 접속 시 dns_probe_finished_nxdomain 에러 처리

맥(Mac)에서 사용자 지정 localhost 호스트 도메인 수정하기

localhost를 기본 설정으로 사용하면 문제없이 로컬 서버에 접속이 가능하지만 local.page.com 등과 같이 임의로 주소를 설정하게 되면 위와 같이 사이트에 연결할 수 없다는 DNS 에러가 발생합니다.

기본적으로 localhost의 주소는 127.0.0.1으로 설정되어 있으므로 localhost를 주소로 사용하면 문제없지만 localhost의 주소명이 바뀐다면 별도의 설정을 통해 알려주어야 합니다.

맥에서 설정 파일은 다음 위치에 있습니다.

/private/etc/hosts

이 파일을 vi 에디터를 통해 설정해보겠습니다(관리자 권한 필요).

% sudo vi /private/etc/hosts

명령어를 실행하고 패스워드를 입력하면 다음과 같은 화면이 나옵니다.

여기에 임의의 주소명과 이 주소명이 찾아갈 IP를 넣어주면 됩니다.

i 키를 누르면 편집이가능하며 앞에는 host 주소 127.0.0.1, 뒤에는 사용할 임의의 주소명을 넣어보겠습니다.

입력 후 저장을 위해 ESC를 누른 뒤 :를 입력, 다시 콘솔에 qw! 를 입력하고 엔터를 치면 저장이 완료됩니다.

완료 후 로컬 서버를 실행하고 local.page.com에 접속하면 문제없이 접속되는 것을 확인할 수 있습니다.

rgba를 사용해 opacity의 상속 문제 해결(투명도 조절)

자식 노드에 상속되는 opacity와 단독적인 rgba

부모 노드에 opacity 효과를 주면 자식 노드에도 상속되어 투명 효과가 함께 적용됩니다.

자동으로 상속이 되므로 다음과 같이 표시됩니다.

아래 설정으로 색상을 지정한 결과입니다.
배경 black
opacity:0.5
background:yellow
background:red

보다시피 yellow, red에 opacity가 적용되어 색상이 흐려진 것을 볼 수 있습니다.

그렇다면 이 상속 문제를 해결하기 위해서는 어떻게 해야 할까요?

간단하게 opacity 대신 rgba를 사용하면 됩니다.

투명도를 포함하는 색상의 지정은 다음과 같습니다.

rgba(R, G, B, opacity);

다음과 같이 사용할 수 있습니다.

위 박스에 해당하는 색상인 #9ADFFD을 rgba 방식으로 표현해보겠습니다.

16진수를 10진수로 변형한 값인 9A=154, DF=223, Fd=253을 각각 R, G, B 위치에 넣어주고 마지막 인수는 0~1 사이의 값을 넣어주면 됩니다.

background : rgba(154, 223, 253, 0.5);

이제 자식 노드는 opacity가 적용되지 않으므로 색상이 선명해진 것을 확인할 수 있습니다.

검은 배경을 사용해도 자식 노드에 선명도가 살아난 것을 확인할 수 있으며 이를 통해 상속되지 않는다는 것을 알 수 있습니다.

테스트에 사용한 코드는 다음과 같습니다.

<div class="div0">
    <div class="div1">
      modify
    </div>
    <div class="div2">
      delete
    </div>
</div>

.div0{
  height:100px;
  background : rgba(0, 0, 0, 0.5);
  display:flex;
  justify-content:center;
  align-items:center;
}

.div1{
  width:200px;
  height:50px;
  background-color:gold;
  display:flex;
  justify-content:center;
  align-items:center;
  color:white;
}

.div2{
  width:200px;
  height:50px;
  background-color:red;
  display:flex;
  justify-content:center;
  align-items:center;
  color:white;
}

웹스톰(Webstorm)에서 prettier 활성화하기

웹스톰에서 prettier를 설정하는 방법

웹스톰(webstorm)에는 기본적으로 prettier가 설치되어 있습니다.

그러므로 npm 또는 yarn을 통해서 node_modules에 prettier를 설치해주고 prettier관련 설정만 해주면 됩니다.

npm install --save-dev --save-exact prettier

위와 같이 설치해준 뒤 File -> Settings -> Languages & Frameworks -> JavaScript -> Prettier에 들어가서 다음과 같이 설정해줍니다.

Prettier package에는 node_modules/prettier가 위치하는 주소를 지정하고 아래 체크 박스를 모두 체크해주면 저장 시에도 prettier가 작동이 됩니다.

작동이 안될 때는 웹스톰을 재시작.

prettier 작업의 단축키는 alt+ctrl+shift+p 입니다.

interface, type 차이점이 뭘까(Typescript)

비슷하지만 동일하지 않은 interface vs type

타입스크립트에서 타입을 선언하기 위해서는 interface 또는 type 키워드를 사용합니다.

각각의 선언 방식은 다음과 같으며 =의 유무만 다릅니다.

// interface
interface Team{
  name : string
}

// type
type Team = {
  name : string
}

그리고 타입의 확장 방법은 다음과 같습니다.

// interface
interface City extends Team {
  city : string
}

// type
type City = Team & {
  city : string
}

interface와 type의 가장 큰 차이점은 바로 선언적 확장(Declaration Merging) 기능인데요.

선언적 확장이란 이미 선언된 타입 선언에 필드를 추가하는 것입니다.

하나는 가능하고 하나는 가능하지 않은데 interface만 가능한 것이 특징입니다.

사용 방법은 다음과 같습니다.

interface Team {
  name : string
}

interface Team {
  manager : string
}

위와 같은 방법으로 이미 선언된 interface에 다시 필드를 선언할 수 있습니다.

하지만 type을 다음과 같은 방법으로 사용하면 에러가 발생합니다.

type Team = {
  name : string
}


// Error 발생 -> 'Duplicate identifier 'Team'
type Team = {
  manager : string
}

추가로 type은 원시형(number, string 등) 데이터를 다른 이름으로 지정해서 사용할 수 있지만 interface는 불가능합니다.

자세한 내용은 다음 코드와 같습니다.

//type으로 원시형 데이터의 이름을 지정
type NameDataType = string;

const printName = (name : NameDataType ) => {
  console.log(name);
}

//interface는 불가
interface NameType extends string {
}

차이점을 신경써도 되지 않을 상황이라면 취향에 따라 선택하면 되지만 기본적으로는 interface를 쓰면 큰 문제가 없다고 합니다.

참고 : typescript 공식 문서

Emotion(Styled), Props 전달하기(Typescript)

Typescript와 함께 동적 CSS 스타일 사용하기
import styled from "@emotion/styled";

const StyledPropsTest = styled.div`
  width: 120px;
  height: 40px;
  font-size: 25px;
  background: ${(props) => (props.testProps ? "blue" : "red")};
  color: white;
`;

const StyledTest = ({testprops}) => {
  return(
    <StyledPropsTest testProps={testprops}>Prop Test</StyledPropsTest>
  )
}

export default StyledTest;

컴포넌트 내부에서 사용하는 변수를 CSS에 전달하여 동적 스타일을 적용하기 위해서는 위와 같이 props를 전달하는 방법이 있습니다.

하지만 Typescipt를 사용하여 custom props를 사용하면 다음과 같은 에러를 만나게 됩니다.

(property) testProps: boolean
Type ‘{ children: string; testProps: boolean; }’ is not assignable to type ‘IntrinsicAttributes & { theme?: Theme | undefined; as?: ElementType<any> | undefined; } & ClassAttributes<HTMLDivElement> & HTMLAttributes<…>’.
Property ‘testProps’ does not exist on type ‘IntrinsicAttributes & { theme?: Theme | undefined; as?: ElementType<any> | undefined; } & ClassAttributes<HTMLDivElement> & HTMLAttributes<…>’.ts(2322)

이는 타입스크립트에서 props의 타입을 체크하기 때문인데요.

다음과 같이 간단하게 <{data:type}>의 타입 단언(Type Assertion)으로 해결할 수 있습니다.

const StyledPropsTest = styled.div<{testProps:boolean}>`
  width: 120px;
  height: 40px;
  font-size: 25px;
  background: ${(props) => (props.testProps ? "blue" : "red")};
  color: white;
`;

다음과 같이 에러가 사라진 것을 확인할 수 있습니다.

leaflet, 반복되는 지도에 marker 표시하기(react)

맵 축소 시 반복되는 지도에 Marker 반영하기

다른 옵션을 설정하지 않는 한 맵을 최대로 축소하면 다음과 같이 지도가 반복되어 나타납니다.

여기서 맵을 오른쪽이나 왼쪽으로 넘겨서 중심을 바꿔도 Marker는 그대로 있습니다.

하지만 맵 렌더링 시 다음과 같이 ‘worldCopyJump’ 옵션을 넣어주면 중심이 이동하더라도 기존의 Marker를 모두 표시할 수 있습니다.

<MapContainer
   center={[35.102, 129.067]}
   zoom={5}
   scrollWheelZoom={true}
   worldCopyJump
>

옵션 하나만으로 맵을 이동하면 Marker도 이동하여 같은 좌표에 표시되는 것을 확인할 수 있습니다.

leaflet 맵 표시 언어 변경

지도 데이터 서버 변경하기

leaflet의 기본 맵은 각 지역마다 현지 나라의 언어로 표기되어 한국 맵은 한국어, 일본 맵은 일본어, 프랑스 맵은 프랑스어로 표기되어 있습니다.

하지만 전체 맵을 영어로 표기하고 싶거나 산맥, 해양 등 용도에 맞게 지도의 이미지를 변경하고 싶을 때는 아래 링크에서 원하는 맵의 유형과 서버를 확인하여 서버만 변경해주면 됩니다.

1. 링크에서 서버 확인

http://leaflet-extras.github.io/leaflet-providers/preview/

링크에 접속하면 다음과 같은 화면입니다.

초록색 네모칸에서 서버 정보, 빨간색 네모칸에서 맵의 종류 샘플을 볼 수 있습니다.

샘플로 OpenStreetMap.France를 적용해보도록 하겠습니다.

빨간 네모칸에서 OpenStreetMap.France을 선택하면 초록 네모칸에 서버 정보가 표시됩니다.

‘https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png’

‘&copy; OpenStreetMap France | &copy; <a href=”https://www.openstreetmap.org/copyright”>OpenStreetMap</a> contributors’

이 두 부분만 코드에서 변경해주면 바로 적용이 됩니다.

2. 코드 적용

import { MapContainer, TileLayer, useMap, Marker, Popup } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import { icon } from "leaflet";
const Icon = icon({
  iconUrl: "marker-icon.png",
  iconSize: [16, 16],
  iconAnchor: [12, 16],
});

const MyMap = () => {
  return (
    <MapContainer
      center={[37.56675, 126.97842]}
      zoom={10}
      scrollWheelZoom={true}
      style={{ width: "500px", height: "500px" }}
    >
      <TileLayer
        attribution='&copy; OpenStreetMap France | &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png"
      />
      <Marker position={[37.56675, 126.97842]} icon={Icon}>
        <Popup>서울시청이에요.</Popup>
      </Marker>
    </MapContainer>
  );
};

export default MyMap;

설정에 따라 변경된 맵과 언어를 확인할 수 있습니다.