Typescript 기초

HughKim·2024년 6월 4일
0

Nextjs14

목록 보기
3/6
post-thumbnail

프론트엔드 개발자로서 사용자에게 안정적인 UX를 제공하는 것은 매우 중요합니다. 안정적인 사용자 경험을 통해 신뢰감을 주고 더 나은 웹사이트를 만들 수 있기 때문입니다. 그리고 높은 코드 품질로 웹사이트를 개발하면 사용자가 버그 없이 웹사이트를 경험할 수 있습니다. 이는 기업의 이미지에 긍정적인 영향을 줄 수 있습니다.
Typescript basic_github link

TypeScript 등장 배경

안정적인 개발과 코드 품질 향상을 위해 TypeScript를 사용하는 것이 좋습니다. TypeScript는 동적 타입 언어인 JavaScript를 보완하며 등장했습니다.

그렇다면 TypeScript는 새로운 언어일까요? 아닙니다. TypeScript는 JavaScript의 상위 집합으로, JavaScript 엔진을 사용하며 개발자가 원하는 변수의 타입을 정의할 수 있습니다. 그리고 이렇게 작성된 TypeScript 코드는 JavaScript로 컴파일되어 실행됩니다.

TypeScript & Javascript

TypeScript와 JavaScript의 차이점을 간단히 살펴보겠습니다.

1. 정적 타입 vs 동적 타입

TypeScript는 "정적 타입 언어"이고, JavaScript는 "동적 타입 언어"입니다.
JavaScript는 변수의 타입이 실행 시간에 결정되는 동적 타입 지정 방식을 사용합니다. 이는 쉽게 배우고 유연하다는 장점이 있지만, 런타임 에러가 발생할 가능성이 있습니다. 특히 대규모 프로젝트에서는 모듈 간 오류 등을 찾아내기 어려울 수 있습니다.
반면 TypeScript는 컴파일 단계에서 타입을 체크하고, 타입 오류를 미리 알려줍니다. 이를 통해 개발 과정에서 빠르게 오류를 찾아내고 수정할 수 있어, 사전에 버그를 방지할 수 있습니다.
이처럼 TypeScript의 정적 타입 시스템은 JavaScript의 동적 타입 지정 방식이 가진 단점을 보완하고자 등장했습니다. 개발자는 TypeScript를 통해 보다 안전하고 생산적인 코드 작성이 가능합니다.

2. 컴파일 언어 vs 인터프리터 언어
JavaScript와 TypeScript는 언어를 해석하는 과정에서 중요한 차이가 있습니다.
JavaScript는 "인터프리터 언어"입니다. 인터프리터 언어는 소스코드를 한 줄씩 읽어가며 즉시 실행합니다. 이로 인해 코드가 빨리 실행되는 장점이 있습니다.
반면 TypeScript는 "컴파일 언어"입니다. 컴파일 언어는 변수의 타입을 명시적으로 지정해야 합니다. 컴파일 단계에서 타입을 체크하기 때문에, 코드 작성 중에 타입 오류를 미리 알려줄 수 있습니다. 이를 "타입 어노테이션"이라고 합니다. 이를 통해 버그를 빨리 발견하고 방지할 수 있습니다.

이처럼 JavaScript와 TypeScript는 언어의 근본적인 특성이 다르기 때문에, 개발 과정과 결과물에서 차이가 있습니다. 개발자는 이러한 차이를 잘 이해하고 프로젝트의 특성에 맞는 언어를 선택해야 합니다.

TypeScript 특징

  1. TypeScript의 강력한 타입 시스템은 사전에 버그를 예방할 수 있는 핵심 요소입니다.
  2. TypeScript를 사용하면 가장 먼저 마주하게 되는 것이 바로 "타입"입니다. 타입은 변수의 "자료형"을 의미합니다. 문자형, 숫자형, 불리언, 객체, 배열 등 변수가 어떤 형태의 데이터를 가지고 있는지를 나타내는 추상적인 개념입니다.
  3. TypeScript는 정적 타입 기반 언어입니다. 즉, 컴파일 과정에서 변수의 타입이 결정됩니다. 이를 통해 컴파일 단계에서 타입 오류를 미리 발견할 수 있습니다. 따라서 TypeScript를 사용하면 컴파일 에러를 사전에 예방할 수 있고, 디버깅 작업도 훨씬 수월해집니다.

이처럼 TypeScript의 강력한 타입 시스템은 안정적인 코드 작성을 가능하게 해줍니다. 변수의 타입을 명확히 정의함으로써 예기치 못한 버그를 사전에 차단할 수 있는 것이 가장 큰 장점이라 할 수 있습니다. 이런 장점 이외에 Typescript만의 장점을 다른 것도 확인해보겠습니다.

TypeScript 장점

  • TypeScript는 높은 생산성을 제공합니다. JavaScript로 코드를 작성하면 객체의 필드나 함수의 매개변수로 들어오는 값이 어떤 것인지 파악하기 위해 여러 파일을 확인해야 하는 단점이 있습니다. 하지만 TypeScript를 사용하면 변수와 데이터의 자료형을 쉽게 파악할 수 있습니다.

  • TypeScript를 사용하면 개발자는 개발의 큰 구조를 구성하고 핵심 부분에 집중할 수 있습니다. 객체 내부의 값을 전부 기억할 필요가 없어지며, 자동으로 리스트업 해주기 때문에 생산성이 향상됩니다. 이러한 생산성 향상은 예기치 못한 동작을 사전에 방지할 수 있습니다.

  • TypeScript는 JavaScript의 상위 집합이기 때문에, 기존에 JavaScript로 구성된 프로젝트를 쉽게 마이그레이션할 수 있습니다. 대규모 코드 수정 없이도 TypeScript로의 전환이 가능합니다.

  • 프로젝트 전체를 한 번에 TypeScript로 전환하는 것이 부담스럽다면, 일부 기능에만 한정하여 점진적으로 적용할 수 있습니다. 이렇게 단계적으로 전환을 진행하면 효율적으로 TypeScript를 도입할 수 있습니다. 추후 필요에 따라 점차 나머지 부분으로 TypeScript 적용 범위를 확장해 나갈 수 있습니다.

TypeScript는 자료형 정보를 명시적으로 제공하여 개발 생산성을 높이고, 잠재적인 버그를 사전에 차단할 수 있습니다. 이를 통해 개발자는 핵심 로직에 더욱 집중할 수 있게 되는 것입니다.
이처럼 TypeScript는 기존 JavaScript 코드와의 호환성이 뛰어나, 프로젝트의 점진적인 마이그레이션을 가능하게 합니다. 이를 통해 개발팀은 큰 부담 없이 TypeScript의 장점을 활용할 수 있게 됩니다.

그렇다고 TypeScript에 단점이 없는 것은 아닙니다. TypeScript 역시 장단점이 존재합니다.

TypeScript 단점

  • 학습에 어려움이 있고 생산성도 저하 될 수 있습니다. TypeScript를 처음 접하는 개발자의 경우, 타입 시스템 등 새로운 개념을 익히는 데 어려움을 겪을 수 있습니다. 이로 인해 초기 생산성이 저하될 수 있습니다. TypeScript의 사용이 익숙하지 않은 개발자들은 타입 관련 학습 시간이 필요하며, 이는 프로젝트 진행 속도에 영향을 미칠 수 있습니다.

  • 타입 정의 및 지정의 어려움이 있습니다. 복잡한 프로젝트에서는 적절한 타입 정의가 쉽지 않을 수 있습니다. 개발자는 매번 타입을 지정해줘야 하는 번거로움을 겪을 수 있습니다. 또한 타입 어노테이션으로 인한 타입 에러 발생 등으로 실제 개발보다 타입을 지정하는 데 더 많은 시간을 소비할 수 있습니다.

따라서 TypeScript 도입 시에는 이러한 학습 곡선과 타입 정의 및 지정의 어려움을 고려해야 합니다. 개발팀의 역량과 프로젝트의 복잡도에 따라 TypeScript의 장단점을 균형 있게 평가하고, 적절한 적용 방안을 모색해야 합니다.

TYPE지정

TypeScript 도입을 위해 기존 JavaScript와 React JSX 파일을 각각 .ts와 .tsx로 변경합니다. 이를 통해 변수와 함수의 타입을 명시하거나 자동으로 정의할 수 있어, 정적 타입 검사로 개발 생산성과 코드 품질을 높일 수 있습니다.

TypeScript는 정적 타입 시스템을 제공하여 변수 선언 시 명시한 타입과 다른 값을 대입하려 하면 컴파일 단계에서 오류를 발생시킵니다. 이를 통해 개발 과정에서 발생할 수 있는 타입 관련 문제를 사전에 방지할 수 있습니다.

String

let variable = "hello";
variable = 15; // fail
variable = "world"; // pass

변수로 선언한 것은 문자열(string)입니다. 하지만 fail로 처리된 부분은 문자열이 아닌 숫자(number) 타입입니다. 이 경우, TypeScript는 "number 형식은 string 형식에 할당할 수 없습니다"라는 type에러를 발생시킵니다.

Number

let age = 20;
age = "20"; // fail
age = 15; // pass

String & Number Type정의

let testString: string;
	testString = 20; // fail
	testString = "hello world"; // pass

let ageWithType: number;
	ageWithType = "hello world"; // fail
	ageWithType = 20; // pass

let testBoolean: boolean;
	testBoolean = "world" // fail
	testBoolean = 2 // fail
	testBoolean = false; // pass

변수 뒤의 ":"는 타입을 정의하는 것으로, 정의한 타입과 변수에 할당된 값이 다를 경우 타입 에러가 발생합니다.

Union Type

let testStringOrNumber: string | number; 
	testStringOrNumber = 10; // pass
	testStringOrNumber = "hello world"; // pass
	testStringOrNumber = false; // fail

유니온 타입은 자바스크립트의 OR(||) 연산자와 같이 "A 아니면 B"라는 의미를 가진 타입을 말합니다. "|" 연산자를 사용해 다양한 타입을 결합하는 이 방법을 유니온 타입 정의라고 합니다.
위의 예시는 string 또는 number인 경우는 type에서 pass하지만 이외 경우 타입이 일치하지 않기 때문에 오류를 발생합니다.

Array

let names = ["jenny", "jane", "tom"];
	names.push(3); // fail
	names.push("john"); // pass

let numbers = [10, 20, 30];
	numbers.push("john"); // fail
	numbers.push(40); // pass

[string] 타입은 names에 추가되는 항목도 문자열이어야 함을 의미합니다. 위의 코드에서는 배열에 문자열 타입이 지정되어 있으며, 숫자를 배열에 추가하려고 하면 타입 에러가 발생하는 것을 확인할 수 있습니다.

Array Type정의

let testStringArray: string[];
	testStringArray = [1, 2, 3]; // fail
	testStringArray = ["jan", "Feb", "Mar"]; // pass
    
let testNumberArray: number[];
	testNumberArray = ["jan", false, 20]; // fail 
	testNumberArray = [1, 2, 3]; // pass
    
let testStringOrNumberArray: (string | number)[]; 
	testStringOrNumberArray = [1, 2, "string"]; // pass
	testStringOrNumberArray = [1, 2, "string", true]; // fail

number[]으로 type선언한 부분에서 fail을 보면 number을 제외한 문자열,불리언이 들어오면 타입에러가 발생하는 것을 볼 수 있습니다. 유니온 타입으로 선언한 값에서 fail이 발생한 이유는 string과 number이외에 boolean값이 들어갔기 때문입니다.

Object

let user = {
  username: "john",
  age: 222,
  isAdmin: false,
};

	user.username = "admin"; // pass
	user.age = "20"; // fail 
	user.age = 20; // pass
	user.isAdmin = true; // pass
	user.phone = "+8110123445678" // fail 

에러가 발생한 이유는 user 객체 내에서 phone 항목에 대한 타입이 명시되지 않았기 때문입니다. user 객체에서 phone의 값으로 number가 선언되어 있으므로, 값을 변경하려면 같은 타입을 유지해야 합니다.

Object Type정의

let userObj: {
  username: string;
  age: number;
  isAdmin: boolean;
};

	userObj = {
 		username: "john",
 		age: 20,
  		isAdmin: false,
   };

userObj에서 정의한 타입에 맞지 않으면 형식 오류가 발생합니다. 모든 속성이 올바르게 작성되어야만 통과됩니다.

선택적 인자

let userObj2: {
  username: string;
  age: number;
  isAdmin: boolean;
  phone?: string; // ?: 선택적 인자
};
	userObj2 = {
  		username: "hugh",
  		age: 20,
  		isAdmin: false,
	};

선택적 인자는 "?:"로 타입 앞에 표기하며, 해당 항목이 있을 수도 있고 없을 수도 있는 상황에서 유연하게 대처하기 위해 사용됩니다. 이렇게 구성되면 함수는 유연성을 가지게 되어 더 나은 개발 효율성을 기대할 수 있습니다. 예를 들어, phone이 선택적 인자로 정의되어 있다면, phone이 있어도 되고 없어도 되며, 있을 경우 문자열을 가질 것임을 의미합니다.

Any

let testAny;
	testAny = "john"; // pass
	testAny = 12; // pass
	testAny = true; // pass
	testAny = ["john"]; // pass
	testAny = [true, 20, "John"]; // pass
	testAny = {}; // pass

let testAnyArray: any[];
	testAnyArray = [20, true, "coke", []]; // pass

any 타입은 어떤 타입도 지정할 수 있어, 일반 JavaScript와 다르지 않게 됩니다. 이렇게 되면 TypeScript를 사용하는 이유가 사라지기 때문에, any 타입을 무분별하게 사용하는 것은 추천하지 않습니다. 이는 TypeScript에서도 권장되지 않습니다.

Functions

let sayHi = () => {
  console.log("hello world!");
};
	sayHi = "hello world!"; // fail

let funReturnString = (): string => {
  console.log("hello world!");
  return "world";
};

let multiple = (num: number) => {
  return num * 2;
};

let multiple2 = (num: number): number => {
  return num * 2;
};

함수에서 반환 타입을 선언하지 않고 반환 값이 없다면, 해당 함수는 void로 선언됩니다. 다시 말해, 함수의 () 뒤에 :type을 선언하면 그 함수는 해당 타입으로 지정된다는 의미입니다. 또한, props가 존재한다면, 해당 props의 항목에 사용하는 부분에도 타입이 필요합니다.

Void

let multiple3 = (num: number): void => {
  console.log(num)
};

let sum = (num1: number, num2: number, another?: number) => {
  return num1 + num2;
};
sum(2, 3);

let func = (user: { username: string; age: number; phone?: string }) => {
  console.log("user: ", user.username);
};

void는 함수에서 반환 값이 없을 경우 사용되는 반환 타입입니다. 이는 함수에 return 문이 없거나 명시적으로 값을 반환하지 않을 때 추론되는 타입을 의미합니다.

TypeScript에서 void를 반환 타입으로 지정하면, undefined 외에 다른 값을 반환할 수도 있습니다. 하지만, 일반적으로 void는 반환 값이 없다는 것을 나타내며, 실제로 반환 값을 사용하지 않음을 의미합니다. 따라서, void는 함수가 반환 값을 사용하지 않을 것이라는 것을 명시적으로 나타내기 위해 사용됩니다.

추가적으로, TypeScript에서는 void를 사용하여 반환 값을 명확히 하지 않는 함수를 정의함으로써 코드의 명료성을 높일 수 있습니다. 이는 함수가 부수 효과(side effect)만을 위해 호출된다는 것을 나타내는 좋은 방법입니다.

Type Aliases

type UserType = {
  username: string;
  age: number;
  phone?: string;
};

let betterFunc = (user: UserType) => {
  console.log(user.username);
};

type myFunc = (a: number, b: string) => void;

let write: myFunc = (num, str) => {
  console.log(num + "times" + str);
};

type UserType2 = {
  username: string;
  age: number;
  phone?: string;
  theme: "dark" | "light";
};

const userWithTheme: UserType2 = {
  username: "hugh",
  age: 20,
  theme: "dark",
};

타입 별칭(Type Aliases)은 새로운 타입 값을 생성하는 것이 아니라, 이미 정의된 타입에 대하여 나중에 쉽게 참조할 수 있도록 이름을 지정하는 방법입니다. 타입 별칭과 인터페이스(Interface) 사이의 주요 차이점 중 하나는 타입의 확장 가능성 여부에 있습니다. 인터페이스는 확장할 수 있어서, 기존의 인터페이스에 새로운 속성이나 메서드를 추가하는 것이 가능하지만, 타입 별칭은 한 번 선언되면 그 형태를 변경할 수 없습니다.

따라서, 가능한 경우 인터페이스를 사용하여 타입을 선언하는 것이 좋습니다. 이는 코드의 유연성을 높이고, 나중에 타입을 확장하거나 수정할 필요가 있을 때 보다 쉽게 대응할 수 있게 해줍니다. TypeScript에서 타입 별칭은 특정한 경우에 유용하게 사용될 수 있지만, 일반적으로는 인터페이스를 통한 타입 선언을 권장합니다.

Interface


interface IUser {
  username: string;
  email: string;
  age: number;
}

interface IEmployee extends IUser {
  employeeId: number;
}

const emp: IUser = {
  username: "tom",
  email: "tom@gmail.com",
  age: 20,
};

const emp2: IEmployee = {
  username: "tom",
  email: "tom@gmail.com",
  age: 20,
  employeeId: 10,
};

TypeScript에서 인터페이스는 코드의 재사용성을 높이고, 명확한 계약을 정의하는 데 중요한 역할을 합니다. 특히, 인터페이스를 이용한 상속 기능은 여러 인터페이스의 특성을 결합하여 새로운 타입을 정의할 수 있는 강력한 방법을 제공합니다. 그리고 extends 키워드를 사용하는 인터페이스 상속은 다음과 같은 이점을 가지고 있습니다.

  • 다중 상속 지원 : 인터페이스는 다른 여러 인터페이스들의 속성을 상속받아, 복합적인 타입을 만들 수 있습니다. 예를 들어, interface A extends B, C라고 선언하면, 인터페이스 A는 B와 C의 모든 멤버를 상속받게 됩니다.
  • 코드 재사용성 증가 : 기존 인터페이스의 속성을 재정의할 필요 없이, 상속을 통해 새로운 인터페이스를 쉽게 확장할 수 있습니다. 이는 코드의 중복을 줄이고, 유지 관리를 용이하게 합니다.
    인터페이스 상속을 통해, 개발자는 더 명확하고 강력한 타입 계약을 정의할 수 있습니다. 이는 프로젝트의 안정성과 코드의 가독성을 향상시킵니다.

결과적으로 TypeScript의 인터페이스는 개발자가 더욱 강력하고 유연한 코드를 작성할 수 있게 도와줍니다. 따라서 인터페이스의 확장 기능은 TypeScript를 사용하는 데 있어서 중요한 개념 중 하나입니다.

Generics

interface IAuthor {
  id: number;
  username: string;
}

interface ICategory {
  id: number;
  title: string;
}

interface IPost {
  id: number;
  titles: string;
  desc: string;
  extra: IAuthor[] | ICategory[];
}

interface IPostBetter<T> {
  id: number;
  title: string;
  desc: string;
  extra: T[];
}

const testMe: IPostBetter<String> = {
  id: 1,
  title: "post title",
  desc: "post description",
  extra: ["str1", "str2"],
};

interface IPostEvenBetter<T extends object> {
  id: number;
  title: string;
  desc: string;
  extra: T[];
}

const testMe2: IPostEvenBetter<{ id: number; username: string }> = {
  id: 1,
  title: "post title",
  desc: "post description",
  extra: [
    {
      id: 1,
      username: "tom",
    },
  ],
};

const testMe3: IPostEvenBetter<IAuthor> = {
  id: 1,
  title: "post title",
  desc: "post description",
  extra: [
    {
      id: 1,
      username: "tom",
    },
  ],
};

const testMe4: IPostEvenBetter<ICategory> = {
  id: 1,
  title: "post title",
  desc: "post description",
  extra: [
    {
      id: 1,
      title: "category title",
    },
  ],
};

TypeScript의 제네릭은 타입을 고정된 값이 아닌, 변할 수 있는 '변수'처럼 다루어 코딩의 유연성을 극대화하는 기능입니다. 이를 통해 개발자는 더 일반화된 코드를 작성할 수 있으며, 이는 코드의 재사용성과 타입 안정성을 동시에 증가시킵니다.

제네릭을 사용하면, 꺾쇠(<>) 안에 변수명을 넣어 타입 변수를 선언할 수 있습니다. 이때, 자주 사용되는 변수명으로는 T가 있는데, 이는 Type의 약자로 볼 수 있습니다. 하지만, T 외에도 U, V 등 개발자가 원하는 어떤 이름도 사용할 수 있습니다.

예를 들어, 배열의 모든 요소를 포함하는 함수를 만들 때, 제네릭을 사용하면 다양한 타입의 배열에 대해 같은 함수를 사용할 수 있습니다. 이는 함수나 클래스 등을 다양한 타입에 대해 재사용 가능하게 만들어 줍니다.

위 코드에서 T는 사용자가 함수를 호출할 때 결정되는 타입으로, 이는 제네릭이 코드를 더 유연하게 만드는 방법 중 하나입니다.
제네릭을 사용함으로써, TypeScript 개발자는 타입 안정성을 유지하면서도 다양한 타입에 대해 동작하는 재사용 가능한 컴포넌트를 생성할 수 있습니다. 이는 크게 코드의 품질을 향상시키고, 개발 과정을 더욱 효율적으로 만듭니다.

React

ReactNode Type

const Parent = ({ children }: { children: React.ReactNode }) => {
  return (
    <div>
      <h1>This is the parent</h1>
      {children}
    </div>
  );
};

export default Parent;


const ChildrenPropExample = () => {
    return (
        <div>
            <Parent>
                <Child />
                <SecondChild />
            </Parent>
        </div>
    );
};

export default ChildrenPropExample;

ReactNode 타입은 JSX 내에서 사용할 수 있는 모든 요소의 타입을 포괄합니다. 이는 ReactChild, ReactFragment, ReactPortal을 포함하며, 또한 boolean, null, undefined 값까지 다룹니다. 이로 인해 ReactNode는 매우 유연하며 다양한 형태의 자식 요소를 JSX 내에서 표현할 수 있게 해줍니다.

클래스 컴포넌트에서는 render 함수가 이 ReactNode 타입을 반환하는 것이 일반적입니다. 이는 클래스 컴포넌트가 화면에 렌더링할 내용을 정의할 때 ReactNode를 사용하여 다양한 형태의 출력을 지원한다는 것을 의미합니다.

반면에, 함수 컴포넌트는 ReactElement 인터페이스를 반환합니다. 이 차이는 ReactElement가 보다 구체적인 JSX 요소를 나타내는 반면, ReactNode는 더 넓은 범위의 타입, 즉 JSX에서 사용될 수 있는 거의 모든 것을 포괄한다는 점에서 비롯됩니다.

이러한 특성 덕분에, React 개발자들은 다양한 타입의 자식 요소들을 효과적으로 관리하고 조합할 수 있으며, 이는 React 애플리케이션의 유연성과 확장성을 크게 향상시킵니다.

parent안에 들어가는 children같은 경우 parent에서 React.ReactNode로 type을 정의했기 때문에 문제없이 props로 child가 들어가는 것을 확인 할 수 있습니다.

Event Type

const EventExample = () => {
    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        console.log(e.target.value);
    };

    const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
        e.preventDefault();
        console.log("Searched!");
    };

    const handleDelete = (e: React.MouseEvent<HTMLButtonElement>, id: number) => {
        e.preventDefault();
        console.log(`Post ${id} has been deleted!`);
    };

    return (
        <div className="eventExample">
            <form>
                <input type="text" placeholder="Search for anything..." onChange={handleChange} />
                <button onClick={handleClick}>Search</button>
            </form>
            <form className="post">
                <h1>{"타입스크립트의 이벤트 항목(1)"}</h1>
                <p>
                    Lorem ipsum dolor sit amet consectetur adipisicing elit.
                </p>
                <button onClick={(e) => handleDelete(e, 1)}>Delete</button>
            </form>
            <form className="post">
                <h1>{"타입스크립트의 이벤트 항목(2)"}</h1>
                <p>
                    Lorem ipsum dolor sit amet consectetur adipisicing elit.
                </p>
                <button onClick={(e) => handleDelete(e, 2)}>Delete</button>
            </form>
        </div>
    );
};

export default EventExample;

React에서는 다양한 이벤트 핸들러를 제공하며, 대표적으로 onClick과 onChange 이벤트 핸들러를 사용할 수 있습니다.

onClick 이벤트의 경우, 주로 HTML의 button 태그 요소와 함께 사용되므로 HTMLButtonElement 타입이 필요합니다. 또한, 마우스 이벤트가 발생할 때는 React.MouseEvent 타입을 사용합니다. 따라서 onClick 이벤트의 타입은 "React.MouseEvent"에 HTMLButtonElement가 됩니다.

반면에, onChange 이벤트는 일반적으로 HTML의 input 태그 요소와 함께 사용되며, 이 경우 HTMLInputElement 타입이 필요합니다. 입력 이벤트가 발생할 때는 React.ChangeEvent 타입을 사용하므로, onChange 이벤트의 타입은 "React.ChangeEvent"의 HTMLInputElement 가 됩니다.

특정 액션(예: 삭제 또는 선택)을 구분해야 할 경우, 이벤트와 함께 ID를 전달할 수 있습니다. 이때 해당 ID의 타입도 명확히 정의해 주어야 합니다. 이를 통해 이벤트 핸들러는 더욱 명확하고 타입 안전성을 보장할 수 있습니다.

이러한 타입 정의를 통해 React 이벤트 핸들러는 보다 견고하고 유지보수하기 쉬운 코드 작성을 가능하게 합니다.

UseRef Type

const UseRefExample = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  const usernameInputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  const handleClick = () => {
    console.log("username is: " + usernameInputRef.current?.value);
  };

  return (
    <div className="useRefExample">
      <input ref={inputRef} type="text" placeholder="focus here" />
      <input ref={usernameInputRef} type="text" placeholder="username" />
      <button onClick={handleClick}>Send</button>
    </div>
  );
};

export default UseRefExample;

useRef를 사용할 때는 참조하는 HTML 태그의 타입을 정확히 지정하는 것이 중요합니다. 예를 들어 input 태그를 참조한다면 HTMLInputElement 타입을 사용해야 하고, button 태그를 참조한다면 HTMLButtonElement 타입을 사용해야 합니다.

이렇게 적절한 타입을 지정하면 코드의 타입 안전성이 높아지고, 참조 객체의 속성과 메서드를 보다 안전하게 활용할 수 있습니다. 타입을 명확히 지정함으로써 컴파일 타임에 오류를 사전에 방지할 수 있고, 코드 자동 완성 기능을 통해 개발 효율성도 높일 수 있습니다.

따라서 개발 과정에서 이 점을 잘 기억하고 적용하면 보다 견고하고 유지보수하기 쉬운 React 애플리케이션을 만들 수 있습니다. 타입스크립트의 강력한 타입 시스템을 최대한 활용하는 것이 핵심입니다.

UseState Type

interface UserType {
    sessionId: number;
    name: string;
}

const UseStateExample = () => {
    const [username, setUsername] = useState("");
    const [user, setUser] = useState<UserType | null>(null);
    // OR
    // const [user, setUser] = useState<UserType>();

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setUsername(e.target.value);
    };

    const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
        e.preventDefault();
        setUser({
            name: username,
            sessionId: Math.random(),
        });
    };

    const handleClickLogout = (e: React.MouseEvent<HTMLButtonElement>) => {
        e.preventDefault();
        setUser(null);
    };

    return (
        <div className="useStateExample">
            {user ? (
                <>
                    <b>{user.name} logged in Typescript </b>
                    <button onClick={handleClickLogout}>Logout</button>
                </>
            ) : (
                <form>
                    <input type="text" placeholder="Username" onChange={handleChange} />
                    <button onClick={handleClick}>Login</button>
                </form>
            )}
        </div>
    );
};

export default UseStateExample;

useState 훅을 사용할 때, 상태 값의 타입을 명확히 정의하는 것은 매우 중요합니다. 예를 들어, 상태 값이 UserType이 될 수도 있고 null일 수도 있는 경우와, 상태 값이 항상 UserType이어야 하고 초기 값에 null을 주지 않는 경우 두 가지로 나눌 수 있습니다. 이 부분은 상황에 맞게 타입을 정의하면 됩니다.
위 코드를 기준으로 설명하도록 하겠습니다.

  • UserType 또는 null을 허용하는 경우
    상태 값이 UserType이거나 초기 상태로 null일 수 있음을 명시합니다. 예를 들어, 사용자가 로그인하기 전에는 null로 설정하고 로그인 후에는 UserType 객체로 설정할 수 있습니다.
  • 항상 UserType만을 허용하는 경우
    상태 값이 항상 UserType이어야 하며, 초기 값도 UserType 객체로 설정합니다. 예를 들어, 애플리케이션이 시작될 때 기본 사용자 정보를 로드하여 상태에 설정할 수 있습니다.

UseContext & UseReducer Type

interface StateType {
    theme: string;
    fontSize: number;
}

interface ColorActionType {
    type: "CHANGE_THEME";
}
type SizeActionType = {
    type: "CHANGE_FONTSIZE";
    payload: number;
};

type ActionType = ColorActionType | SizeActionType;

export const ThemeContext = createContext<{
    state: StateType;
    dispatch: React.Dispatch<ActionType>;
}>({
    state: INITIAL_STATE,
    dispatch: () => {},
});

useReducer를 사용할 때, state와 dispatch 모두에 대해 타입을 정의하는 것이 중요합니다. state는 상태 항목에 들어가는 부분의 타입을 정의하고, dispatch는 action을 실행하기 때문에 React.Dispatch를 사용하여 ActionType을 정의합니다.

  • 상태와 액션 타입 정의 : 먼저 상태 타입과 액션 타입을 정의해야 합니다.
interface StateType {
    theme: string;
    fontSize: number;
}

interface ColorActionType {
    type: "CHANGE_THEME";
}
type SizeActionType = {
    type: "CHANGE_FONTSIZE";
    payload: number;
};

type ActionType = ColorActionType | SizeActionType;
  • 리듀서 함수 정의 : 리듀서 함수는 상태와 액션을 받아 새로운 상태를 반환합니다.
const reducer = (state: StateType, action: ActionType) => {
  switch (action.type) {
    case "CHANGE_THEME":
      return {
        ...state,
        theme: state.theme === "dark" ? "light" : "dark",
      };
    case "CHANGE_FONTSIZE":
      return {
        ...state,
        fontSize: action.payload,
      };

    default:
      return state;
  }
};
  • useReducer 훅 사용 : useReducer 훅을 사용할 때 초기 상태와 리듀서 함수를 전달합니다. 이때 state와 dispatch의 타입이 자동으로 유추됩니다.
const INITIAL_STATE = {
  theme: "dark",
  fontSize: 20,
};

export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
  const [state, dispatch] = useReducer(reducer, INITIAL_STATE);

  return <ThemeContext.Provider value={{ state, dispatch }}>{children}</ThemeContext.Provider>;
};
  • 컴포넌트에서 사용 : state와 dispatch를 컴포넌트 내에서 사용하여 상태를 관리하고 액션을 디스패치할 수 있습니다.
const UseContextExample = () => {
    const { state, dispatch } = useContext(ThemeContext);
    console.log(state);

    return (
        <div className="useContextExample">
            <button onClick={() => 
            dispatch({ type: "CHANGE_THEME" })}>Change Theme</button>
            <button onClick={() => 
            dispatch({ type: "CHANGE_FONTSIZE", payload: 20 })}>Change Font Size</button>
        </div>
    );
};

export default UseContextExample;

useReducer를 사용할 때 상태와 액션의 타입을 명확히 정의하면, 코드의 타입 안전성을 높이고, 유지보수성 및 가독성을 향상시킬 수 있습니다.

profile
성장에 미쳐버린 Frontend Developer

0개의 댓글