TypeScript : basic types

55555-JyeonΒ·2024λ…„ 1μ›” 16일
0

What's

λͺ©λ‘ 보기
4/7
post-thumbnail
post-custom-banner

🧐 μ‹€λ¬΄μ—μ„œ 자주 μ‚¬μš©ν•˜λŠ” νƒ€μž… λ°”λ‘œ μ•ŒκΈ° (1/3)

νƒ€μž…μŠ€ν¬λ¦½νŠΈμ—λŠ” ꡉμž₯히 λ§Žμ€ νƒ€μž…λ“€μ΄ μ‘΄μž¬ν•˜κ³  μ‹€μ œλ‘œλŠ” 자주 μ‚¬μš©λ˜μ§€ μ•ŠλŠ” νƒ€μž…λ“€λ„ 많이 μ‘΄μž¬ν•©λ‹ˆλ‹€.
μ•„λž˜λŠ” λ§Žμ€ κ°œλ°œμžλ“€μ΄ μ‹€λ¬΄μ—μ„œ μ‚¬μš©ν•˜λ©΄μ„œ κ°„ν˜Ή ν˜Ήμ€ 자주 μ‚¬μš©ν•˜λŠ” νƒ€μž…λ“€ 쀑 κΈ°λ³Έ νƒ€μž…λ“€μž…λ‹ˆλ‹€.




ν‘œλ‘œ μ •λ¦¬ν•œ κΈ°λ³Έ νƒ€μž…

λͺ‡ 가지λ₯Ό μ œμ™Έν•˜λ©΄ μžλ°”μŠ€ν¬λ¦½νŠΈμ—μ„œλ„ 자주 μ‚¬μš©ν•˜λŠ” νƒ€μž…λ“€μ΄κΈ° λ•Œλ¬Έμ— 낯섀지 μ•Šμ•„ μ‰½κ²Œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€ :)


Basic Types
type desc.
Number 뢀동 μ†Œμˆ˜ 값을 λ°μ΄ν„°λ‘œ κ°–λŠ” νƒ€μž…
Boolean μ°Έ/거짓(true/false) 값을 λ°μ΄ν„°λ‘œ κ°–λŠ” νƒ€μž…
String ν…μŠ€νŠΈ 데이터 νƒ€μž…
Any νƒ€μž… 검사λ₯Ό ν•˜μ§€ μ•Šκ³ , κ·Έ 값듀이 컴파일 μ‹œκ°„μ— 검사λ₯Ό ν†΅κ³Όν•˜κΈΈ 원할 λ•Œ μ‚¬μš©ν•˜λŠ” νƒ€μž… (ꢌμž₯ X)
Object μ›μ‹œ νƒ€μž…μ΄ μ•„λ‹Œ νƒ€μž…
Array 값을 λ°°μ—΄λ‘œ μ‚¬μš©ν•  수 있게 ν•΄μ£ΌλŠ” νƒ€μž…
Unknown 아직 μ–΄λ–€ νƒ€μž…μ΄ λ“€μ–΄μ˜¬μ§€ λͺ¨λ₯Ό 경우, λ‹€μ–‘ν•œ νƒ€μž…μ„ 집어넣어야할 경우 μ‚¬μš©ν•˜λŠ” νƒ€μž…
Union OR(||) μ—°μ‚°μžμ™€ λΉ„μŠ·ν•œ 의미λ₯Ό 가지며 μ§€μ •λœ νƒ€μž…μ˜ κ°’λ§Œ μ‚¬μš© κ°€λŠ₯
Conditional 쑰건뢀 νƒ€μž…, νƒ€μž… 관계 κ²€μ‚¬λ‘œ ν‘œν˜„λœ 쑰건에 따라 두 가지 κ°€λŠ₯ν•œ νƒ€μž… 쀑 ν•˜λ‚˜λ₯Ό 선택
Type Alias μƒˆλ‘œμš΄ νƒ€μž…μ„ μ •μ˜, interface둜 ν‘œν˜„ν•  수 μ—†κ±°λ‚˜ μœ λ‹ˆμ˜¨ λ˜λŠ” νŠœν”Œμ„ μ‚¬μš©ν•΄μ•Όν•  λ•Œ μ‚¬μš©
Interface ⭐️ ν•¨μˆ˜μ˜ param에 객체의 속성 νƒ€μž…μ„ μ •μ˜ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 상속을 톡해 ν™•μž₯이 ν•„μš”ν•  λ•Œ μ‚¬μš©


πŸ” μ˜ˆμ‹œλ‘œ 더 μžμ„Ένžˆ μ‚΄νŽ΄λ³΄λŠ” κΈ°λ³Έ νƒ€μž…


Number

뢀동 μ†Œμˆ˜ 값을 λ°μ΄ν„°λ‘œ κ°–λŠ” νƒ€μž…μž…λ‹ˆλ‹€.

function addNumber(num1: number, num2: number) {
  return num1 + num2;
}
addNumber(5, 10);  // 15

Boolean

μ°Έ/거짓(true/false) 값을 λ°μ΄ν„°λ‘œ κ°–λŠ” νƒ€μž…μž…λ‹ˆλ‹€.

let isLogin: boolean = false;

if (isLogin) {
    console.log('μ–΄μ„œμ˜€μ„Έμš”! μ˜€λŠ˜λ„ μ°Ύμ•„μ£Όμ…”μ„œ κ°μ‚¬ν•©λ‹ˆλ‹€ :)');
} else {
    console.log('κ°€μž…λœ 정보가 μ—†μŠ΅λ‹ˆλ‹€, νšŒμ›κ°€μž… ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?');
}

String

ν…μŠ€νŠΈ 데이터 νƒ€μž…μž…λ‹ˆλ‹€.

function addString(str1: string, str2: string):string {
  return str1 + str2;
}
addString("μ•ˆλ…•", "ν•˜μ„Έμš”");
addString("hello", "world");

function welcomeMessage(name: string): string {
    return `${name}λ‹˜ μ˜€λŠ˜λ„ 1commit ν•΄λ΄μš”!`;
}

console.log(welcome('Mobi'));

Any

νƒ€μž… 검사λ₯Ό ν•˜μ§€ μ•Šκ³ , κ·Έ 값듀이 컴파일 μ‹œκ°„μ— 검사λ₯Ό ν†΅κ³Όν•˜κΈΈ 원할 λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€.
any νƒ€μž…μ˜ μ‚¬μš©μ€ νƒ€μž…μŠ€ν¬λ¦½νŠΈμ˜ 의미λ₯Ό ν‡΄μƒ‰λ˜κ²Œ ν•˜λŠ” 쑴재둜 ꢌμž₯ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

let isLoggedIn: any = "뭐가 μ˜€λ“  였λ₯˜ μ•ˆ 남";

Object

μ›μ‹œ νƒ€μž…μ΄ μ•„λ‹Œ νƒ€μž…μ— μ‚¬μš©ν•©λ‹ˆλ‹€.
μ›μ‹œ νƒ€μž… πŸ‘‰ number, string, boolean, bigint, symbol, null, λ˜λŠ” undefined

const userInfo: object = { id: "user-01", password: "12345" };

Array

값을 λ°°μ—΄λ‘œ μ‚¬μš©ν•  수 있게 ν•΄μ£ΌλŠ” νƒ€μž…μž…λ‹ˆλ‹€.

type Mobi = {
    userName: string,
  	gender: string,
  	term: number,
  	isOperator: boolean,
};
// 06-1. λ°°μ—΄ μš”μ†Œλ“€μ„ λ‚˜νƒ€λ‚΄λŠ” νƒ€μž… 뒀에 []

let members: Mobi[] = [
	{
    	userName: 'Kei',
      	gender: female,
  		term: 0,
  		isOperator: true,
    },
	{
    	userName: 'Jane',
      	gender: female,
  		term: 1,
  		isOperator: true,
    },
  	{
    	userName: 'Heeppy',
      	gender: female,
  		term: 1,
  		isOperator: true,
    },

];
// 06-2. μ œλ„€λ¦­ λ°°μ—΄ νƒ€μž… (Array<elementType>)

let members: Array<Mobi> = [
  	{
    	userName: 'Jack',
      	gender: male,
  		term: 2,
  		isOperator: false,
    },
	{
    	userName: 'Levi',
      	gender: male,
  		term: 2,
  		isOperator: false,
    },
  	{
    	userName: 'Noel',
      	gender: male,
  		term: 2,
  		isOperator: false,
    },
];

Unknown

아직 μ–΄λ–€ νƒ€μž…μ΄ λ“€μ–΄μ˜¬μ§€ λͺ¨λ₯Ό 경우, λ‹€μ–‘ν•œ νƒ€μž…μ„ 집어넣어야할 경우 μ‚¬μš©ν•©λ‹ˆλ‹€.
unknown νƒ€μž…μ—” λͺ¨λ“  자료 λ‹€ 집어넣을 수 있으며 자료λ₯Ό 넣어도 νƒ€μž…μ€ κ·ΈλŒ€λ‘œ unknown이 μœ μ§€λ©λ‹ˆλ‹€.
any와 λ‹€λ₯Έ 점은 unknown νƒ€μž…μ—λŠ” 아무것도 ν•  수 μ—†λ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€.
anyλŠ” μ–΄λ–€ νƒ€μž…μ΄ μ˜€λ“  κ°€λŠ₯ν•˜λ‹€, unknown은 μ–΄λ–€ νƒ€μž…μ΄ μ˜¬μ§€ λͺ¨λ₯΄κ² λ‹€μ˜ λ‰˜μ•™μŠ€μž…λ‹ˆλ‹€.

let userID: unknown;

userID = true;  // boolean κ°’ κ°€λŠ₯
userID = 123;	// number κ°’ κ°€λŠ₯
userID = "hello";  // string κ°’ κ°€λŠ₯

" μ™œ anyλŠ” ꢌμž₯ν•˜μ§€ μ•ŠλŠ”λ° unknown은 별닀λ₯Έ 말이 μ—†λ‚˜μš”? "

anyλŠ” λͺ¨λ“  νƒ€μž…κ³Ό 연산을 ν—ˆμš©ν•˜μ§€λ§Œ, unknown은 νƒ€μž…μ΄ 뭔지 λͺ¨λ₯΄κΈ° λ•Œλ¬Έμ— μ•„λ¬΄λŸ° 연산도 μˆ˜ν–‰ν•˜μ§€ λͺ»ν•©λ‹ˆλ‹€.
unknown은 any에 λΉ„ν•΄ μ•ˆμ •μ μΈ νƒ€μž…μœΌλ‘œ 생각할 수 있으며 unknown으둜 연산을 μˆ˜ν–‰ν•˜λ €λ©΄ λͺ…μ‹œμ μœΌλ‘œ νƒ€μž…μ„ 확인해야 ν•©λ‹ˆλ‹€.


Union

OR(||) μ—°μ‚°μžμ™€ λΉ„μŠ·ν•œ 의미λ₯Ό 가지며 μ§€μ •λœ νƒ€μž…μ˜ κ°’λ§Œ μ‚¬μš© κ°€λŠ₯ν•©λ‹ˆλ‹€.


let age: string | number;
age = 20;
age = "Twenty";

function loginData(nickName: string, pw: string | number) {
  // code ...
  console.log(nickName, pw);
  alert(`${nickName}λ‹˜, μ–΄μ„œμ˜€μ„Έμš”!`);
}
loginData("Jack", "1234");
loginData("Levi", 4567);
type UXDesigner = {
    nickName: string,
    tool: string,
  	type: 'UX'
};
type UIDesigner = {
    nickName: string,
    tool: string,
  	type: 'UI'
};
type FEDeveloper = {
    nickName: string,
    language: string,
  	type: 'FE'
};
type BEDeveloper = {
    nickName: string,
    language: string,
  	type: 'BE'
};
function ClassifyOccupation(sb: UXDesigner | BEDeveloper) {
    if ('language' in sb) {
        // sb TDesigner λΌλŠ” 뜻
        alert(`${sb}λ‹˜μ€ 개발자 μ’…μ‚¬μžμž…λ‹ˆλ‹€.`)
    }else{
      alert(`${sb}λ‹˜μ€ λ””μžμ΄λ„ˆ μ’…μ‚¬μžμž…λ‹ˆλ‹€.`)
    }
} 

// typeofλŠ” μ›μ‹œκ°’λ§Œ 검사 κ°€λŠ₯
function ClassifyDeveloper(sb: FEDeveloper | BEDeveloper) {
  if (sb.type === "FE") {
      alert(`${sb}λ‹˜μ€ Frontend 개발자 μž…λ‹ˆλ‹€.`)
  }else{
    alert(`${sb}λ‹˜μ€ Backend 개발자 μž…λ‹ˆλ‹€.`)
  }
} 

Conditional

쑰건뢀 νƒ€μž…, νƒ€μž… 관계 κ²€μ‚¬λ‘œ ν‘œν˜„λœ 쑰건에 따라 두 가지 κ°€λŠ₯ν•œ νƒ€μž… 쀑 ν•˜λ‚˜λ₯Ό μ„ νƒν•©λ‹ˆλ‹€.
μžλ°”μŠ€ν¬λ¦½νŠΈμ—μ„œ μ‚Όν•­μ—°μ‚°μž 같은 μ—­ν• μž…λ‹ˆλ‹€.

T extends U ? X : Y πŸ‘‰ (Tκ°€ U에 ν• λ‹Ή κ°€λŠ₯ν•œμ§€μ— λ”°λΌμ„œ X와 Y쀑 ν•˜λ‚˜μ˜ νƒ€μž…μ„ 선택)

type IsString<T> = T extends string ? "yes" : "no";
type sample1 = IsString<"κ°€λ‚˜λ‹€λΌ">; // yes
type sample2 = IsString<1234>; //no
type sample3 = IsString<true>; //no

// 'T'와 'U'κ°€ 같은 νƒ€μž…μ΄λ©΄ 'Yes' νƒ€μž…μ„, λ‹€λ₯Έ νƒ€μž…μ΄λ©΄ 'No' νƒ€μž…μ„ λ°˜ν™˜
type IsSameType<T, U> = T extends U ? (U extends T ? "Yes" : "No") : "No";
type Result1 = IsSameType<string, string>; // 'Yes'
type Result2 = IsSameType<string, number>; // 'No'
type Result3 = IsSameType<"a" | "b", string>; // 'No'
// conditional type으둜 formatted-date λ§Œλ“€κΈ°
type DateFormat = 'YYYY-MM-DD' | 'MM/DD' | 'HH:mm';

type GetElements<T> = T extends 'Y' 
	? 'year' 
	: T extends 'M' 
	? 'month' 
	: T extends 'D'
	? 'date'
	: T extends 'H'
	? 'hour'
	: T extends 'm'
	? 'minute'
	: never;

type SplitDateReturn <Format extends DateFormat> = Format extends `${infer C}${infer Rest}`
	? { [E in GetElements<C> | keyof SplitDateReturn<Rest>]: number }

declare function splitDate<Format extends DateFormat>(date: Date, format: Format):SplitDateReturn<Format>;

splitDate(newDate(), 'YYYY-MM-DD')  // {year: number, month: number, date: number}
splitDate(newDate(), 'MM/DD') // {month: number, date: number}
splitDate(newDate(), 'HH:mm') // {hour: number, minute: number}

Type Alias

μƒˆλ‘œμš΄ νƒ€μž…μ„ μ •μ˜ν•˜λŠ” κ²ƒμœΌλ‘œ interface와 μœ μ‚¬ν•œ νŠΉμ§•μ„ κ°–κ³  μžˆμŠ΅λ‹ˆλ‹€.
interface둜 ν‘œν˜„ν•  수 μ—†κ±°λ‚˜ μœ λ‹ˆμ˜¨ λ˜λŠ” νŠœν”Œμ„ μ‚¬μš©ν•΄μ•Όν•  λ•Œ type aliasλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.
μ›μ‹œ νƒ€μž…μ— 별칭을 μ •ν•˜λŠ” 것은 ꢌμž₯ν•˜μ§€ μ•ŠλŠ” μ‚¬ν•­μž…λ‹ˆλ‹€.


// μ›μ‹œ νƒ€μž…μ— 별칭을 μ •ν•˜λŠ” 것은 ꢌμž₯ X
type Name = string;
type Age = number;

const mobi: Name = "Peanut";
const age: Age = 20;
// 객체에 별칭주기
type MobiMember = {
  name: string;
  nickName: string;
  term: number;
  isOperator: boolean;
};

const Amy:MobiMember = {
  name: "μ˜€μ£Όμ—°",
  nickName: "Amy",
  term: 2,
  isOperator: true,
};

Interface ⭐️

ν•¨μˆ˜μ˜ param에 객체의 속성 νƒ€μž…μ„ μ •μ˜ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 상속을 톡해 ν™•μž₯이 ν•„μš”ν•  λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€.

interface Peanut {
  name: string;
  nickName: string;
  age: number;
  isDirector: boolean;
}

function printName(person: Peanut) {
  console.log(person.name); // "κΉ€μ„±μš©"
  console.log(person.nickName); // "Peanut"
  console.log(person.age); // 20
  console.log(person.isDirector); // true
}
interface Developer {
  isMobi: boolean;
  nickName: string;
}

//μΈν„°νŽ˜μ΄μŠ€ ν™•μž₯
interface whichDev extends Developer {
  sort: string;
}
const Zero: Developer = {
  isMobi: true;
  nickName: 'Zero';
};

const Zoey: Developer = {
  isMobi: true;
  nickName: 'Zoey';
};

const Peanut: whichDev = {
  isMobi: true;
  nickName: 'Peanut';
  sort: 'Full';
};

const Rin: whichDev = {
  isMobi: true;
  nickName: 'Rin';
  sort: 'FE';
};

μ•„λž˜μ™€ 같이 keyκ°€ string인 κ²½μš°μ— ν•œμ— λͺ¨λ“  속성듀을 λ‹€ ν—ˆμš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.
인덱슀 μ‹œκ·Έλ‹ˆμ²˜λŠ” λͺ¨λ“  속성듀을 λ‹€ μ •μ˜ν•  수 없을 λ–„ μœ μš©ν•˜κ²Œ 쓰일 수 μžˆμŠ΅λ‹ˆλ‹€.

interface MobiInfo {
  userName: string,
  nickName: string,
  name: string,
  term: number,
  isOperator: boolean,
  [key: string]: any; // index signiture
}
const  Peter: MobiInfo = {
  userName: 'Peter',
  nickName: 'Peanut',
  name: 'κΉ€μ„±μš©',
  term: 0,
  isOperator: true,
  isDirector: true, // index signiture
  hobby: 'coding', // index signiture
  MBTI: 'ENTP', // index signiture
}

const  Daniel: MobiInfo = {
  userName: 'Daniel',
  nickName: 'λ‹€λ…€',
  name: 'μ΄λŒ€κ²½',
  term: 2,
  isOperator: false,
  hobby: 'soccer', // index signiture
  MBTI: 'ENTJ', // index signiture
}

const  Kimi: MobiInfo = {
  userName: 'Kimi',
  nickName: '퀴미,
  name: 'κΉ€κ°€μ˜',
  term: 2,
  isOperator: false,
}

μ–Έμ œ νƒ€μž…μ„ μ¨μ•Όν• κΉŒ?

νƒ€μž…μ„ μ£Όμ–΄μ•Όν•˜λŠ” κ²½μš°μ™€ 주지 μ•Šμ•„λ„ λ˜λŠ” 경우λ₯Ό κ΅¬λΆ„ν•˜κΈ°

⬇️ javascript

let seongyong = {
  age: 20,
  height: 190,
};

function log(obj) {
  console.log(obj.height);
  return obj;
}

const a = log(seongyong);
console.log(a.age);

⬇️ typescript

interface Person = {
  age: number,
  height: number,
};

let seongyong:Person = {
    age: 20,
    height: 190
}

λ§€κ°œλ³€μˆ˜μ˜ νƒ€μž…κ³Ό 리턴 κ°’μ˜ νƒ€μž…μ΄ 동일할 경우 πŸ‘‰ TS 선택

function log(obj: Person) {
  console.log(obj.height);
  return obj;
}

리턴 κ°’μ˜ νƒ€μž…μ„ λ§€κ°œλ³€μˆ˜μ˜ νƒ€μž…λ§ŒμœΌλ‘œλŠ” 좔둠이 μ•ˆ λ˜λŠ” 경우 πŸ‘‰ TS ν•„μˆ˜
ex. νƒ€μž… ν•©μ„±

interface Movvi = {
    nickname: string,
    isDirector: boolean,
}


function log(obj: Person):Person & Movvi{
    return {...obj, nickname : 'Peanut', isDirector: true}
}

νƒ€μž…μ΄ 좔둠이 μΆ©λΆ„νžˆ κ°€λŠ₯ν•  경우(λ§€κ°œλ³€μˆ˜μ™€ 리턴 κ°’μ˜ νƒ€μž…μ΄ λ™μΌν•œ 경우) νƒ€μž…μ„ μ£ΌλŠ” 것은 κ°œλ°œμžλ§ˆλ‹€ 생각이 λ‹€λ₯Ό κ²ƒμž…λ‹ˆλ‹€.
νƒ€μž…μ„ λΆ€μ—¬ν•˜κ²Œ 되면 λΆ€λ“μ΄ν•˜κ²Œ μ½”λ“œμ˜ 길이가 κΈΈμ–΄μ Έ 가독성이 떨어진닀 생각될 μˆ˜λ„ 있고
μ–Έμ–΄ μ„œλΉ„μŠ€λ₯Ό μΆ©λΆ„νžˆ 지원받을 수 있기 λ•Œλ¬Έμ— ꡳ이 쓰지 μ•Šμ•„λ„ λœλ‹€ 생각할 수 μžˆμŠ΅λ‹ˆλ‹€.

λ°˜λŒ€λ‘œ μ½”λ“œμ˜ μ»¨λ²€μ…˜μ΄λ‚˜ ν˜‘μ—…μ„ μœ„ν•΄ 좔둠이 κ°€λŠ₯ν•œ κ²½μš°μ—λ„ νƒ€μž…μ„ μ£ΌλŠ” 것을 μ„ ν˜Έν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.
ν˜‘μ—…ν•˜λŠ” κ°œλ°œμžκ°€ μ–΄λ–€ μ„±ν–₯이냐에 따라 ν”„λ‘œμ νŠΈμ˜ 크기에 따라 λ‹¬λΌμ§ˆ μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

ν˜„μž¬ μ €λŠ” νƒ€μž…μŠ€ν¬λ¦½νŠΈλ₯Ό λ°°μš΄μ§€ 일주일 정도 밖에 λ˜μ§€ μ•Šμ•˜κΈ° λ•Œλ¬Έμ— νƒ€μž…μ„ μ£ΌλŠ” 것이 λ‚«λ‹€κ³  μƒκ°ν•©λ‹ˆλ‹€.
λŸ°νƒ€μž„μ— νƒ€μž…μ΄ μ •μ œλœλ‹€λŠ” 점을 생각해봀을 λ•Œ 아직은 μ΅œλŒ€ν•œ μƒμ„Ένžˆ μ£ΌλŠ” 것이 였λ₯˜λ₯Ό μ΅œμ†Œν™”ν•  뿐만 μ•„λ‹ˆλΌ
νƒ€μž… μ‚¬μš©μ— μ΅μˆ™ν•΄μ§€λŠ” 길이라고 μƒκ°λ©λ‹ˆλ‹€.
λ”°λΌμ„œ νƒ€μž…μ˜ μƒλž΅μ€ νƒ€μž…μŠ€ν¬λ¦½νŠΈλ₯Ό κ³΅λΆ€ν•˜κ³  손에 많이 μ΅μ—ˆλ‹€κ³  생각될 λ•Œ ν•˜λŠ” 것이 쒋을 것 κ°™μŠ΅λ‹ˆλ‹€ :)



References
https://frontj.com/entry/%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%83%80%EC%9E%85-%EC%A0%95%EB%B3%B5-Conditional-Types

profile
πŸ₯ž Stack of Thoughts
post-custom-banner

0개의 λŒ“κΈ€