[유데미x스나이퍼팩토리] 프로젝트 캠프 : Next.js 2기 - 5일차(DOM, 이벤트, 타입스크립트)

김하은·2024년 7월 20일
0

개발자가 되고 싶어

목록 보기
10/12
post-thumbnail

오늘은 본가에 가야 해서 온라인으로 수업을 들었다. DOM, 이벤트, 타입스크립트에 대해 배웠는데, 프레임워크를 배우고 나서부터 DOM에 대한 내용을 많이 잊어버려 복습할 수 있어 의미 있었다. 하지만 내용이 방대해서 간략하게만 짚고 넘어가서 지난번에 공부하고 기록해 놓은 블로그 글을 다시 읽어보면서 부족한 부분을 채웠다. 또한, 간단한 문제를 풀었는데 생각보다 쉽지 않아서 충격이었다. 바닐라 자바스크립트를 연습해야 할 이유가 생겼다.

타입스크립트는 여러 프로젝트에서 사용했지만, 다시 공부하고 싶은 부분 중 하나였다. 특히 제네릭 사용이 많이 어려워서 그 부분에 집중해서 들었다. 또한, 강사님은 interface대신 type만 사용한다고 하신 부분이 인상깊었는데, 나도 프로젝트 때 마다 관련 주제로 논쟁을 했었는데 강사님의 주장과 탄탄한 근거를 들으니 설득됐다. 배운 내용을 잊어버리지 않도록 정리하려 한다.

18. DOM(Document Object Model)

  • 웹 개발에서 자바스크립트는 HTML 요소를 조작하고, 사용자 인터랙션을 처리하는 도구임
  • DOM은 HTML 요소를 객체로 표현하여 자바스크립트로 접근하고 조작할 수 있게 해줌
  • 웹 브라우저는 HTML을 파싱하여 DOM을 생성하고, 이를 통해 자바스크립트로 HTML 요소에 접근하고 조작할 수 있음

문서 객체 찾는 법 (DOM 조작의 시작)

1. querySelector

querySelector 메서드는 CSS 선택자와 동일한 방법으로 HTML 요소를 한 개만 찾을 때 사용

const h1El = document.querySelector('h1');
console.log(h1El); // <h1>가장 먼저 찾아지는 요소를 반환함

2. querySelectorAll

querySelectorAll 메서드는 CSS 선택자와 동일한 방법으로 여러 HTML 요소를 찾을 때 사용

const h1Els = document.querySelectorAll('body > h1');
console.log(h1Els);

문서 바꾸기

HTML 요소의 내용을 바꾸는 방법

const h1El = document.querySelector('h1');
h1El.innerHTML = '<i>sucoding</i>';
h1El.innerText = '<i>sucoding</i>';
console.log(h1El);

스타일 주는 법

CSS 스타일을 직접 설정할 수 있음

const h1El = document.querySelector('h1');
h1El.style.color = 'red';
h1El.style.fontSize = '130px';
console.log(h1El);

클래스 추가하는 법

HTML 요소에 클래스를 추가하는 방법

const h1El = document.querySelector('h1');
h1El.classList.add('active'); // 기존 클래스 뒤에 추가
console.log(h1El);

클래스 제거하는 법

HTML 요소에서 클래스를 제거하는 방법

const h1El = document.querySelector('h1');
h1El.classList.remove('active');
console.log(h1El);

클래스 토글하기

클래스가 있으면 제거하고, 없으면 추가하는 방법

const h1El = document.querySelector('h1');
h1El.classList.toggle('done');
console.log(h1El);

input에 입력된 값 가져오기

input 요소에 입력된 값을 가져오는 방법

setTimeout(() => {
  const inputEl = document.querySelector('input');
  console.log(inputEl.value);
}, 3000);

➕ 더 자세한 내용은 지난번에 작성한 글 참고하기


20. 이벤트

➕ 더 자세한 내용은 지난번에 작성한 글 참고하기

❓아래 코드에서 this가 가리키는 요소는?

const buttonEl = document.querySelector('button');
buttonEl.addEventListener('click', function (ev) {
  console.log(this); // 클릭된 button 요소
  console.log(ev);   // 이벤트 객체
  console.log('click');
});

❗위 코드에서 thisbuttonEl을 가리킴

  • 이는 이벤트 핸들러 함수가 일반 함수로 정의되었기 때문임
  • 이때, this는 이벤트가 바인딩된 요소를 참조함
  • 만약 화살표 함수를 사용하면 this는 다르게 동작함
  • 화살표 함수에서는 this가 상위 스코프의 this를 유지함
const buttonEl = document.querySelector('button');
buttonEl.addEventListener('click', (ev) => {
  console.log(this); // 상위 스코프의 this
  console.log(ev);   // 이벤트 객체
  console.log('click');
});
  • 위 코드에서 this는 상위 스코프의 this를 유지하므로, 전역 객체(window)를 가리킬 수 있음

타입스크립트

타입스크립트는 자바스크립트에 타입 시스템을 추가하여 코드의 안정성과 가독성을 높이는 도구임

타입 추론 (Type Inference)

  • 타입스크립트는 변수의 타입을 추론할 수 있음
  • 변수를 선언할 때 타입을 명시하지 않아도, 할당된 값을 통해 타입을 추론함

  • 위 코드에서 타입스크립트는 str 변수가 문자열 타입임을 추론함
  • str2변수는 const로 선언 돼 변수의 타입을 문자열 리터럴 "hello"로 추론함

타입 명시 (Type Annotation)

  • 변수를 선언할 때 명확하게 타입을 명시할 수도 있음
  • 변수명 뒤에 콜론(:)을 붙이고 타입을 명시함
const num: number = 10;
const arr: number[] = [1, 2, 3];
const arr2: [number, string, number] = [1, "A", 3]; // 튜플(tuple)
const arr3: (string | number)[] = [1, "A", 3];

const obj: {} = {};
const obj2: { name: string; age: number } = { name: "kim", age: 20 };

리터럴 타입

특정 값만 가질 수 있는 타입

let num: 10 | 20 = 10;
let str: "A" | "B" = "A";

let obj: { name: "kim" } = { name: "kim" };

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

printName("kim");
printName(obj.name);

타입 오퍼레이터

타입을 조작하는 연산자

유니온 타입

OR 연산자(|)를 사용하여 여러 타입 중 하나를 선택할 수 있음

const arr: (number | string)[] = [1, "A", 3];

인터섹션 타입

AND 연산자(&)를 사용하여 여러 타입을 조합할 수 있음

const obj: { name: string } & { age: number } = { name: "kim", age: 20 };

인터페이스 (Interface)

객체의 구조를 정의하는 방법

  • 객체의 형태를 명확하게 정의하고, 코드의 가독성과 안정성을 높일 수 있음

기본 사용법

  • 인터페이스를 정의하고 이를 사용하여 객체의 타입을 명시할 수 있음
  • 암묵적으로 대문자 I를 붙여주는 관행이 있음
interface IUser {
  name: string;
  age: number;
}

const user: IUser = {
  name: "John",
  age: 30,
};

console.log(user.name); // "John"
console.log(user.age); // 30

옵셔널 프로퍼티(Optional Property)

  • 인터페이스에서 옵셔널 프로퍼티를 정의할 수 있음
  • 객체가 해당 속성을 가질 수도 있고, 가지지 않을 수도 있음을 나타냄
interface IUser {
  name: string;
  age: number;
  height?: number; // 옵셔널 프로퍼티
}

const user1: IUser = {
  name: "John",
  age: 30,
};

const user2: IUser = {
  name: "Jane",
  age: 25,
  height: 170,
};

console.log(user1.height); // undefined
console.log(user2.height); // 170

읽기 전용 속성 (Readonly Properties)

  • 읽기 전용 속성은 객체가 생성된 후에 값을 변경할 수 없도록 함
  • readonly 키워드를 사용하여 읽기 전용 속성을 정의함
interface IUser {
  name: string;
  readonly age: number; // 읽기 전용 속성
}

const user: IUser = {
  name: "John",
  age: 30,
};

user.name = "Jane"; // 가능
user.age = 25; // 오류 발생: age는 읽기 전용 속성입니다.

인터페이스 상속 (Interface Inheritance)

  • 인터페이스는 다른 인터페이스를 상속받을 수 있음
  • 이를 통해 인터페이스 간의 재사용성을 높일 수 있음
interface IPerson {
  name: string;
  age: number;
}

interface IEmployee extends IPerson {
  employeeId: number;
}

const employee: IEmployee = {
  name: "John",
  age: 30,
  employeeId: 12345,
};

console.log(employee.name); // "John"
console.log(employee.employeeId); // 12345

인터페이스 병합 (Interface Merging)

  • 타입스크립트에서는 동일한 이름의 인터페이스가 여러 번 정의되면, 자동으로 병합됨
  • 이를 통해 모듈 간에 인터페이스를 확장할 수 있음
interface ICar {
  brand: string;
}

interface ICar {
  model: string;
}

const car: ICar = {
  brand: "Toyota",
  model: "Corolla",
};

console.log(car.brand); // "Toyota"
console.log(car.model); // "Corolla"

인덱스 시그니처 (Index Signatures)

  • 인덱스 시그니처를 사용하면 동적으로 속성 이름을 정의할 수 있음
  • 객체가 특정 패턴을 따르는 속성 이름과 타입을 가질 수 있도록 함
  • 하지만 인덱스 시그니처의 무분별한 사용은 지양해야함
    • 타입 안전성 감소: 객체의 키와 값에 대해 매우 유연한 타입을 허용하므로, 예상치 못한 타입 오류가 발생할 가능성이 있음
    • 자동 완성 및 코드 가독성 저하: 인덱스 시그니처를 사용하면 IDE가 제공하는 자동 완성 기능이 제대로 동작하지 않을 수 있음
    • 유지보수성 문제: 인덱스 시그니처를 사용하면 객체의 구조가 명확하지 않아서, 다른 개발자가 코드를 이해하고 유지보수하는 데 어려움을 겪을 수 있습니다. 명확한 객체 구조를 정의하는 것이 유지보수성을 높이는 데 중요함
interface IStringArray {
  [index: number]: string;
}

const myArray: IStringArray = ["Alice", "Bob", "Charlie"];

console.log(myArray[0]); // "Alice"
console.log(myArray[1]); // "Bob"
  • 인덱스 시그니처는 객체에서도 사용할 수 있음
interface IDictionary {
  [key: string]: string | number;
}

const dict: IDictionary = {
  name: "John",
  age: 30,
  country: "USA",
};

console.log(dict.name); // "John"
console.log(dict.age); // 30

함수 타입 (Function Types)

  • 인터페이스는 함수의 타입을 정의할 수도 있음
interface ISearchFunc {
  (source: string, subString: string): boolean;
}

const mySearch: ISearchFunc = (src, sub) => {
  return src.includes(sub);
};

console.log(mySearch("Hello, world!", "world")); // true
console.log(mySearch("Hello, world!", "typescript")); // false

타입 별칭 (Type Alias)

type 키워드를 사용하여 새로운 타입을 정의하는 방법

  • 복잡한 타입을 간결하게 정의하고, 코드의 가독성을 높일 수 있음
  • 여러 타입을 하나의 이름으로 묶어서 사용할 수 있음
  • 암묵적으로 대문자 T를 붙여주는 관행이 있음

예시 1: 문자열 리터럴 타입

  • 문자열 리터럴 타입을 사용하면 특정 문자열 값만을 허용하는 타입을 정의할 수 있음
type TRainbowColor = "red" | "orange" | "yellow" | "green" | "blue" | "indigo" | "violet";

const phoneColor: TRainbowColor = "indigo";
const wrongColor: TRainbowColor = "pink"; // 오류 발생

  • 위 예시에서 TRainbowColor 타입은 무지개의 색상 중 하나의 값만을 가질 수 있는 타입을 정의함
  • phoneColor 변수는 이 타입을 사용하여 정의되었으며, 올바른 색상 값만을 가질 수 있음

예시 2: 객체 타입

  • 객체 타입을 정의할 때도 타입 별칭을 사용할 수 있음
type TUser = {
  name: string;
};

type TJob = {
  readonly title?: string;
};

type TUserAndJob = TUser & TJob;

const user1: TUserAndJob = {
  name: "kim",
  title: "developer",
};

const user2: TUser = {
  name: "kim",
};
  • TUserAndJob 타입에서 처럼 TUserTJob 타입을 결합(&)하여 두 타입의 속성을 모두 가지는 객체를 정의할 수 있음

예시 3: 함수와 객체 타입

  • 타입 별칭을 사용하여 함수의 매개변수 타입을 정의할 수 있음
type TUser = {
  name: string;
  age: number;
};

const user1: TUser = {
  name: "kim",
  age: 20,
};

function printUserName({ name }: TUser) {
  console.log(name);
}

printUserName(user1); // "kim"

인터페이스와 타입 별칭의 차이점

타입 별칭과 인터페이스는 유사하지만 몇 가지 중요한 차이점이 있음

병합

  • 인터페이스: 동일한 이름으로 여러 번 정의되면 자동으로 병합됨

    interface Car {
      brand: string;
    }
    
    interface Car {
      model: string;
    }
    
    const myCar: Car = {
      brand: "Toyota",
      model: "Corolla",
    };
    
    console.log(myCar.brand); // "Toyota"
    console.log(myCar.model); // "Corolla"
  • 타입 별칭: 동일한 이름으로 여러 번 정의할 수 없음

    type Car = {
      brand: string;
    };
    
    // 다음 줄은 오류 발생
    // type Car = {
    //   model: string;
    // };
    
    const myCar: Car = {
      brand: "Toyota",
      // model: "Corolla", // 오류 발생
    };

상속

  • 인터페이스: 상속을 지원함, 한 인터페이스가 다른 인터페이스를 확장할 수 있음
interface IPerson {
  name: string;
}

interface IEmployee extends IPerson {
  readonly title?: string;
}

const employee: IEmployee = {
  name: "John",
  title: "developer",
};
  • 타입 별칭: 상속을 지원하지 않음, 타입 별칭을 사용할 때는 & 연산자를 사용하여 여러 타입을 결합할 수 있음
type TUser = {
  name: string;
};

type TJob = {
  readonly title?: string;
};

type TUserAndJob = TUser & TJob;

const user1: TUserAndJob = {
  name: "kim",
  title: "developer",
};

툴팁 지원

VS Code에서는 type으로 정의한 타입은 툴팁으로 확인할 수 있지만, 인터페이스로 정의한 타입은 툴팁에서 바로 확인할 수 없는 경우가 있음

  • 인터페이스:

  • 타입 별칭:

제네릭

제네릭은 타입 매개변수를 사용하여 여러 타입을 유연하게 처리할 수 있음

  • 즉, 타입을 미리 지정하지 않고 사용하는 시점에 타입을 정의해서 쓸 수 있는 문법
  • 타입 매개변수는 주로 T를 사용하지만, 다른 이름을 사용할 수도 있음
  • 제네릭을 사용하면 함수, 클래스, 인터페이스, 타입 별칭 등을 작성할 때 구체적인 타입을 나중에 지정할 수 있음

기본 예시

const firstElements = <T>(elements: T[]): T => {
  return elements[0];
};

console.log(firstElements<number>([1, 2, 3])); // 1
console.log(firstElements<string>(["a", "b", "c"])); // "a"
console.log(firstElements<boolean>([true, false])); // true
  • 위 코드에서 firstElements 함수는 배열의 첫 번째 요소를 반환함
  • 함수 정의에서 <T>는 타입 매개변수를 선언한 것임
  • 호출할 때 firstElements<number>([1, 2, 3])와 같이 구체적인 타입을 전달하여 사용할 수 있음

제네릭을 사용한 인터페이스와 클래스

제네릭은 인터페이스와 클래스에서도 사용할 수 있음

인터페이스에서 제네릭 사용

// 예시 1
interface Container<T> {
  value: T;
}

const stringContainer: Container<string> = { value: "Hello" };
const numberContainer: Container<number> = { value: 123 };

console.log(stringContainer.value); // "Hello"
console.log(numberContainer.value); // 123
  • Container 인터페이스는 제네릭 타입 T를 가지며, value 속성은 T 타입임
  • 이를 통해 다양한 타입의 값을 가지는 컨테이너를 정의할 수 있음
// 예시 2
type TCar<T> = {
    name: string;
    options: T;
  };

  const car1: TCar<string> = {
    name: "sonata",
    options: "auto",
  };

  const car2: TCar<string[]> = {
    name: "sonata",
    options: ["auto", "sunroof"],
  };

클래스에서 제네릭 사용

class Box<T> {
  contents: T;

  constructor(contents: T) {
    this.contents = contents;
  }

  getContents(): T {
    return this.contents;
  }
}

const stringBox = new Box<string>("Hello");
console.log(stringBox.getContents()); // "Hello"

const numberBox = new Box<number>(123);
console.log(numberBox.getContents()); // 123
  • Box 클래스는 제네릭 타입 T를 가지며, contents 속성과 getContents 메서드는 T 타입임
  • 이를 통해 다양한 타입의 값을 가지는 박스를 정의할 수 있음

제네릭 타입 제약 (Type Constraints)

  • 제네릭 타입 매개변수에 제약을 추가하여 특정 조건을 만족하는 타입만 허용할 수 있음
  • 이는 extends 키워드를 사용하여 제네릭 타입 매개변수에 제약을 두는 방식으로 구현됨
const getLength = <T extends { length: number }>(item: T): number => {
  return item.length;
};

console.log(getLength([1, 2, 3])); // 3
console.log(getLength("Hello")); // 5
console.log(getLength({ length: 10 })); // 10
// console.log(getLength(10)); // 오류 발생: 'number' 형식에는 'length' 속성이 없음
  • 위 코드에서 getLength 함수는 length 속성을 가진 객체만 받을 수 있도록 제약을 두었음
  • T extends { length: number }T 타입이 length 속성을 가져야 함을 명시함
  • 따라서, 배열, 문자열, length 속성을 가진 객체는 사용할 수 있지만, 숫자와 같은 타입은 사용할 수 없음

제네릭을 활용한 고급 예제

제네릭을 사용한 다중 타입 매개변수

제네릭을 사용할 때, 다중 타입 매개변수를 정의할 수도 있음

function mapPair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}

const pair1 = mapPair<string, number>("age", 30);
const pair2 = mapPair<number, boolean>(1, true);

console.log(pair1); // ["age", 30]
console.log(pair2); // [1, true]
  • 위 코드에서 mapPair 함수는 두 개의 타입 매개변수 KV를 가지며, keyvalue의 타입을 각각 KV로 지정함
  • 반환 값은 [K, V] 타입의 튜플임

제네릭을 사용한 인터페이스 확장

제네릭 인터페이스를 다른 인터페이스가 확장할 수 있음

interface Response<T> {
  data: T;
  status: number;
  error?: string;
}

interface User {
  name: string;
  age: number;
}

const userResponse: Response<User> = {
  data: { name: "John", age: 30 },
  status: 200,
};

console.log(userResponse.data.name); // "John"
console.log(userResponse.status); // 200
  • Response 인터페이스는 제네릭 타입 T를 가지며, data 속성의 타입을 T로 지정함
  • 이를 통해 다양한 타입의 응답 데이터를 처리할 수 있음

profile
아이디어와 구현을 좋아합니다!

0개의 댓글