Lucian Log
Blog
Computer Science
Algorithm
DB
Network
OS
General
AI
Blockchain
Concurrency
ETC
Git
Infrastructure
AWS
Docker
Java-Ecosystem
JPA
Java
Spring
JavaScript-Ecosystem
JavaScript
Next.js
React
TypeScript
Python-Ecosystem
Django
FastAPI
Python
SQLAlchemy
Software Engineering
Architecture
Culture
Test
Home
Contact
Copyright © 2024 |
Yankos
Home
> JavaScript-Ecosystem
Now Loading ...
JavaScript-Ecosystem
TypeScript basic - Advanced Object
Interface 타입스크립트에서 타입을 정의하는 방법은 다양합니다. type Mail = { postagePrice: number; address: string; } const catalog: Mail = ... 기존처럼 type을 사용해 정의할 수도 있습니다. interface Mail { postagePrice: number; address: string; } const catalog: Mail = ... 그런데 타입스크립트에서 객체의 타입을 정의하는데 자주 사용되는 또 하나의 방법은 interface를 사용하는 것입니다. type과 interface는 문법적인 측면에서 = 사용의 차이가 있지만, 타입을 강제하는 기능은 동일합니다. 그렇다면 interface는 어디에 사용하는 것일까요? type은 object 뿐만 아니라 primitive 타입을 포함한 모든 타입을 정의하는데 사용할 수 있는 반면, interface는 object 타입 정의에만 사용할 수 있습니다. 마치 설계도와 같은 느낌이 녹아 있는 interface는 제약이 있다는 점에서 코드를 일관성 있게 작성하도록 도와주기 때문에, 객체 지향 프로그램을 작성할 때는 interface를 주로 사용합니다. Interfaces and class Interface와 class는 궁합이 잘 맞습니다. Interface는 object의 타입을 정의하는 키워드이고 class는 object로 프로그래밍하는 방법이기 때문입니다. interface Robot { identify: (id: number) => void; } class OneSeries implements Robot { identify(id: number) { console.log(`beep! I'm ${id.toFixed(2)}.`); } answerQuestion() { console.log('42!'); } } interface는 class / object에 타입을 적용할 수 있습니다. 특히, class에 타입을 적용할 때에는 implements 키워드를 사용합니다. 위 코드는 OneSeries 클래스에 implements 키워드를 사용해 Robot 타입을 적용하는 과정입니다. Robot 타입이 적용된 OneSeries는 인터페이스에 명시된 대로 identify 메서드를 가져야 하며, 명시된 것만 지켰다면 이외로 추가적인 answerQuestion 메서드를 가지는 것도 가능합니다. Deep nested type class OneSeries implements Robot { about; constructor(props: { general: { id: number; name: string; } }) { this.about = props; } getRobotId() { return `ID: ${this.about.general.id}`; } } Class OneSeries는 nested된 object 타입을 가지는 about 프로퍼티와 getRobotId 메서드를 가집니다. 이러한 nested된 object 타입을 표현하고 싶다면, interface Robot은 다음과 같이 작성하면 됩니다. interface Robot { about: { general: { id: number; name: string; }; }; getRobotId: () => string; } 타입스크립트는 무한히 nested된 object 타입을 표현할 수 있습니다! 타입 구성 분리하기 interface About { general: { id: number; name: string; version: { versionNumber: number; } } } 위와 같이 더욱 깊게 nested되는 object 타입일수록 가독성은 떨어집니다. 또한, About 타입에서도 version만 필요한 상황이 있을 수 있습니다. 따라서, 일정한 정도로 각각 따로 interface를 만들어 함께 사용하는 것이 효과적일 수 있습니다. interface About { general: General; } interface General { id: number; name: string; version: Version; } interface Version { versionNumber: number; } 앞선 복잡했던 interface 코드를 가독성 높은 재사용가능한 코드로 변형했습니다. 코드는 조금 길어졌지만, 더욱 큰 프로그램에서는 이러한 형태로 코드를 작성하는 것이 훨씬 유리합니다. Extending interface 때때로 어떤 타입의 모든 프로퍼티와 메서드들을 복사해서 다른 타입에 가져와야 할 때도 있습니다. 이 때 extends가 유용합니다. interface Shape { color: string; } interface Square extends Shape { sideLength: number; } const mySquare: Square = { sideLength: 10, color: 'blue' }; Square는 extends 키워드를 사용해 Shape의 모든 프로퍼티를 복사해서 가져옵니다. 실제로 mySquare에서는 sideLength 프로퍼티 뿐만 아니라 color 프로퍼티를 가져도 에러가 나지 않습니다. Index signature 외부의 API나 소스로부터 데이터를 받아오는 경우, 특정 객체의 프로퍼티 이름이 정확히 무엇인지 알 수 없는 상황이 생깁니다. 이 때, 해당 프로퍼티들을 받는 변수를 임의의 이름으로 하나 설정해 처리할 수 있습니다. 이를 index signature라고 합니다. { '40.712776': true; '41.203323': true; '40.417286': false; } 예를 들어, 위와 같은 데이터를 map API query에 대한 response로 받았다고 가정해봅시다. String 타입으로 이루어진 각각의 프로퍼티들은 위도를 나타냅니다. 다만, 이러한 프로퍼티들은 개발자 입장에서 정확히 이름을 알기가 어렵습니다. interface SolarEclipse { [latitude: string]: boolean; } 따라서, 위와 같이 [latitude: string]라는 index signature를 정의해주면, response로 받는 데이터에 존재하는 모든 프로퍼티들의 타입을 하나로 정의할 수 있습니다. 위의 경우 모든 프로퍼티의 이름은 string 타입으로, 그 값은 boolean 타입으로 정의됩니다. latitude는 개발자가 임의로 설정한 이름임을 유의합니다. Optional type member 어떤 함수나 클래스를 만들 때, optional argument 설정은 자유롭습니다. 이는 interface로 타입을 정의할 때도 마찬가지입니다. interface에서도 타입 멤버들에 대해 optional 속성을 설정해 줄 수 있습니다. interface OptionsType { name: string; size?: string; } function listFile(options: OptionsType) { let fileName = options.name; if (options.size) { fileName = `${fileName}: ${options.size}`; } return fileName; } 위의 size 프로퍼티는 optional한 프로퍼티입니다. 프로퍼티 이름과 :사이에 ?가 존재한다면, 해당 프로퍼티는 optional 프로퍼티로 간주됩니다. 따라서, 위 코드에서는 size 프로퍼티를 사용하기 전에 if (options.size) 조건문을 사용해 size 프로퍼티의 존재 여부를 먼저 확인하고 사용해야 합니다. listFile({ name: 'readme.txt' }) size 프로퍼티가 optional하기 때문에, 위와 같이 size 프로퍼티가 없는 객체를 인자로 사용해도 에러를 일으키지 않습니다. Reference Codecademy - TypeScript
JavaScript-Ecosystem
· 2021-10-16
TypeScript basic - Type Narrowing
Type narrowing 타입스크립트는 자신의 소스코드를 자바스크립트 코드로 컴파일하는 단계에서, 타입 체크를 하여 개발자에게 알림을 줍니다. 이러한 컴파일 단계에서의 타입 체크는 매우 유용합니다. 그러나 타입스크립트는 더 많은 것을 제공해줄 능력이 있습니다. 타입스크립트는 코드의 주변 맥락을 확인하여 런타임시 어떻게 동작할지 파악하고, 이에 따라 변수의 구체적인 타입을 추론하여 알려줍니다! 이를 type narrowing이라고 합니다. 특히, 변수가 union을 통해 다양한 타입의 가능성을 내재하고 있을 때, type narrowing은 빛을 발합니다. function formatDate(date: string | number) { // date can be a number or string here if (typeof date === 'string') { // date must be a string here } } 위와 같은 코드에서 date 인자는 string 타입도 number 타입도 가능합니다. 이 때, if (typeof date === 'string')같은 type guard를 사용해 각각의 타입마다 따로 로직을 만들어 type narrowing 할 수 있습니다. 이는 타입스크립트의 런타임 코드 실행 맥락 파악을 통한 타입 추론 능력 덕분입니다! Type guard function formatDate(date: string | number) { // date can be a number or string here if (typeof date === 'string') { // date must be a string type } } 타입스크립트의 type narrowing은 type guard를 통해 진행됩니다. Type guard는 변수의 구체적인 타입을 체크하는 표현식을 가리킵니다. 일반적으로 typeof가 많이 활용됩니다. 위에서 if (typeof date === 'string') 부분이 type guard에 해당됩니다. in as type guard 때때로 특정 프로퍼티 혹은 메서드가 해당 타입에 존재하는지 확인하고 싶은 경우가 있습니다. 이 때, in operator를 사용할 수 있습니다. in은 특정 프로퍼티가 객체 자체에 혹은 해당 객체의 프로토타입 체인 내에 존재하는지 확인해줍니다. type Tennis = { serve: () => void; } type Soccer = { kick: () => void; } function play(sport: Tennis | Soccer) { if ('serve' in sport) { return sport.serve(); } if ('kick' in sport) { return sport.kick(); } } 그리고 in은 type guard로서 사용할 수 있습니다. 위의 if ('serve' in sport)에서는 특정 프로퍼티에 존재 여부가 타입스크립트에게 단서를 주어 type narrowing이 이루어집니다. 위 코드의 경우, 만일 'serve'가 sport에 존재한다면, sport는 Tennis 타입일 것이 확정되기 때문에 if ('serve' in sport) 구문 내에서는 sport를 Tennis 타입 변수로 간주하고 코드를 짜도 무방합니다. 즉, 타입스크립트가 에러를 띄우지 않습니다. Narrowing with else 만일 if 조건문이 type guard로 쓰였다면, 이에 대응하는 else 문은 if 문과 정확히 반대되는 type guard로서 기능합니다. 즉, if 문에서 체크한 타입 이외의 가능한 타입들은 모두 else 문에서 고려하게 됩니다. function formatPadding(padding: string | number) { if (typeof padding === 'string') { return padding.toLowerCase(); } else { return `${padding}px`; } } 예를 들어, 위 if 문에서 string 타입에 대한 로직을 작성했기 때문에, else 문은 number 타입에 대한 로직을 자동으로 담당하게 됩니다. Narrowing After a Type Guard 사실 else 문을 사용하지 않아도 else 문과 똑같은 type narrowing을 사용할 수 있습니다. Type guard인 if 문이 끝난 이후 나오는 코드들은 나머지 가능한 타입들에 대한 코드로 자동으로 상정됩니다. type Tea = { steep: () => string; } type Coffee = { pourOver: () => string; } function brew(beverage: Coffee | Tea) { if ('steep' in beverage) { return beverage.steep(); } return beverage.pourOver(); } 예를 들어, 위의 if 문 내에서는 beverage가 Tea 타입을 가질 것입니다. 반면, if 문이 끝나고 나온 return beverage.pourOver(); 코드에서는 beverage가 당연히 Coffee 타입일 것이기 때문에, 타입스크립트는 오류를 내지 않습니다. Reference Codecademy - TypeScript
JavaScript-Ecosystem
· 2021-10-15
TypeScript basic - Union
Union 타입스크립트는 변수마다 다른 단계의 타입 구체성을 부여할 수 있습니다. 예를 들어, 변수에 string 타입을 강제하면 해당 변수는 string 타입으로 매우 제한적인 타이핑을 가지게 됩니다. 반면에, any를 부여하면 해당 변수는 특정 타입에 제한되지 않는 매우 자유로운 타이핑을 가지게 됩니다. Union 타입은 이러한 두 극단의 타이핑에서 중간을 찾아가는 방법입니다. union은 서로 다른 타입들을 원하는대로 조합하여 만든 것을 의미합니다. 예를 들어, 회사원의 ID를 저장할 때, ID는 string 혹은 number가 모두 올 수 있습니다. 다만, 이를 any로 받기에는 너무 광범위하기 때문에, union을 사용해 원하는 타이핑 범위를 조절하는 것이 효과적입니다. Union 정의 Union은 |을 사용해 원하는 type 멤버들을 하나하나 함께 정의합니다. let ID: string | number; // number ID = 1; // or string ID = '001'; console.log(`The ID is ${ID}.`); 위 코드에서 ID는 string 혹은 number 값의 할당이 모두 허용됩니다. 이러한 union 타입은 함수의 파라미터를 포함해 어디서든 사용할 수 있습니다. function getMarginLeft(margin: string | number) { return { 'marginLeft': margin }; } 예를 들어, 함수의 파라미터에서는 위와 같이 union을 정의해주면 됩니다. Type narrowing with type guard Union을 사용하다보면, 코드의 특정 지점에서 union으로 type annotation된 변수의 타입이 모호해지는 경우가 발생합니다. function getMarginLeft(margin: string | number) { // ... } 예를 들어, 함수 내에서 margin은 string과 number를 동시에 가지기 때문에, string의 메서드를 분별없이 사용하면 타입스크립트 트랜스파일러가 오류를 띄웁니다. 따라서, 다음과 같이 type guard를 사용하여, 해당 지점에서 변수가 string인지 number인지 명확히 표시해주어야 합니다. function getMarginLeft(margin: string | number) { // margin may be a string or number here if (typeof margin === 'string') { // margin must be a string here return margin.toLowerCase(); } } 위의 if 조건문은 type guard라고 부릅니다. 조건문 내에서라면 margin은 반드시 string 타입임이 보장되므로, toLowerCase()와 같은 string 메서드를 써도 에러가 나지 않습니다. 이렇게 type guard를 사용하여 코드 내에서 type을 명확히 하는 것을 type narrowing이라고 합니다. Union을 사용할 때는 type narrowing으로 각각의 타입에 맞는 로직을 분리해 사용하는 것이 필요합니다. Inffered union return type 만일 경우마다 다양한 타입의 값을 리턴하는 함수가 있다면, 타입스크립트는 해당 함수의 return type을 union으로서 판단합니다. function getBook() { try { return getBookFromServer(); } catch (error) { return `Something went wrong: ${error}`; } } 예를 들어, 위 코드에서 getBookFromServer()의 리턴 값의 타입이 Book이라고 합시다. 그러면 함수 getBook은 Book 혹은 string 타입의 값을 리턴할 것입니다. 따라서, 타입스크립트는 getBook의 리턴 타입을 union Book | string으로 추론합니다. Union with array Union 타입은 array와 함께 할 때 더욱 강력해집니다. const dateNumber = new Date().getTime(); // returns a number const dateString = new Date().toString(); // returns a string const timesList: (string | number)[] = [dateNumber, dateString]; 예를 들어, 날짜의 타입으로 number 혹은 string이 올 수있습니다. 이러한 날짜 데이터를 array에 담고 싶다면, 위와 같이 union을 사용해 const timesList: (string | number)[] = [dateNumber, dateString];로 type annotation 해주면 됩니다. 이를 활용하면, 다양한 multiple type을 annotation하여 유연하게 배열을 사용할 수 있습니다. Union with literal type type Color = 'green' | 'yellow' | 'red'; function changeLight(color: Color) { // ... } 프로그램에서 어떠한 구체적으로 구별되는 상태를 만들길 원할 때, literal type을 union을 사용해 만들 수 있습니다. 위와 같이 'green', 'yellow', 'red'라는 리터럴을 사용해 union 타입을 만들면, 'purple'과 같은 인자는 타입스크립트에 의해 validation 됩니다. Reference Codecademy - TypeScript
JavaScript-Ecosystem
· 2021-10-14
Next.js basic - 개념 조각 모음
Next.js 주요한 특징들 Static Generation VS Sever-side Rendering VS Client-side Rendering Static Generation HTML이 build time에 pre-rendering되는 방식입니다. 즉, 외부에서 가져오는 데이터들도 build time에 요청하기 때문에 최신 데이터보다는 잘 변하지 않는 데이터들을 처리하기에 적합합니다. 미리 HTML을 생성하기 때문에 SEO에 강점이 있습니다. 또한 미리 한 번 생성된 HTML을 재사용하는 것과 더불어 Sever-side Rendering과 달리 CDN에 캐시되는 덕분에 셋 중에 속도가 가장 빠르며, Next.js에서 가장 권장되는 방법입니다. Sever-side Rendering HTML이 유저로부터 request가 있을 때마다 pre-rendering되는 방식입니다. 즉, 외부에서 가져오는 데이터들이 request 시점에 요청된 데이터들이기 때문에, 최신 데이터들을 사용하기 용이하다는 장점이 있습니다. HTML이 pre-rendering되기 때문에, SEO에 강점이 있으며, Static Generation보다는 느리지만 Client-side Rendering 보다는 빠릅니다. 다만, 사용자 측면에서는 페이지 이동마다 화면이 깜빡거리며 새로고침이 발생하게 됩니다. Client-side Rendering HTML의 pre-rendering 및 외부 데이터 API 요청을 하지 않고, 클라이언트 측에서 자바스크립트 코드로 모든 것을 처리하는 방식입니다. 사용자가 요청한 페이지만 불러온 후, 사용자의 행동에 따라 필요한 부분만 다시 읽어 들이는 single page application 방식으로 동작하게 됩니다. 따라서, 사용자 측면에서 리로딩없이 필요한 부분만 빠르게 인터랙션할 수 있습니다. 다만, 초기 구동 속도가 느리고 SEO가 어렵다는 단점이 있습니다. (구글에서는 Client-side Rendering도 SEO를 잘 할 수 있다고 이야기하지만, Client-side Rendering은 SEO가 잘 안된다는 것이 정설입니다.) 위의 렌더링 방식들은 페이지마다 다르게 적용할 수 있고, 한 페이지 안에서도 부분마다 다르게 적용할 수 있습니다. 예를 들어, 보통 SEO가 가장 잘되어야 하는 부분은 상품 정보 페이지이므로 해당 페이지는 Static Generation이나 Sever-side Rendering으로 처리하는 것이 좋습니다. 또한, 상품 정보 페이지 내에서 title 같은 정보는 잘 변하지 않으므로 Static Generation을 사용하는 것이 좋습니다. 반면에, description이나 keyword 같은 부분들은 A/B Test 등으로 자주 변화를 시도해 볼 수 있기 때문에, Sever-side Rendering을 사용하는 것이 적합합니다. 이외의 데이터와 상관없는 navigation bar나 메뉴 같은 부분들은 Client-side Rendering을 적용해 보다 나은 인터랙션을 제공할 수 있습니다. Static file serving Next.js는 static 파일을 public 디렉토리에서 처리합니다. 그리고 public 폴더 안에 있는 static file들은 base URL을 /로 사용할 수 있습니다. 예를 들어, /public/me.jpg는 /me.jpg로 사용하면 됩니다. Public 디렉토리에 있는 파일들은 빌드 타임에만 서빙되므로, 런타임에 저장되는 파일들은 AWS S3 같은 다른 서드 파티 서비스를 사용해 처리하길 권장합니다. Reference Next.js Document
JavaScript-Ecosystem
· 2021-10-14
TypeScript basic - Complex Types
1. Array Array의 타입을 정하는 것은 앞서 진행한 primitive types와는 조금 다릅니다. 자바스크립트의 array는 다양한 타입의 데이터가 array의 요소로서 공존할 수 있기 때문입니다. 따라서, array의 타입을 알아낸다는 것은 각각의 element의 타입을 추적한다는 의미가 됩니다. 기존 자바스크립트에서는 array의 타입을 추적하는 작업이 상당히 번거롭지만, 타입스크립트는 이를 간편하게 해결해줍니다. Array type annotation Array의 타입을 type annotation으로 미리 정할 수 있습니다. Array type annotation 방법은 두 가지 존재합니다. let names: string[] = ['Danny', 'Samantha']; 먼저, element 타입을 기존의 type annotation 방법처럼 지정하고 []를 바로 뒤에 붙여주는 방식입니다. 위의 경우, element의 타입이 string인 array가 만들어집니다. let names: Array<string> = ['Danny', 'Samantha']; 또 다른 방법은 Array<T> 문법을 사용하는 것입니다. 이 역시 앞선 코드와 동일하게 array의 element 타입을 string으로 지정합니다. 이렇게 type annotation이된 array는 array를 생성할 때의 element가 지정한 타입과 다르거나 array에 새로 추가하는 element의 타입이 지정한 타입과 다를 때, 타입 에러를 보여줍니다. Multi-dimensional array 다차원 array를 다룰 때는 차원수에 대응하여 []를 type annotation으로 더 붙여주면됩니다. let arr: string[][] = [['str1', 'str2'], ['more', 'strings']]; 예를 들어, string[][]의 경우 (string[])[]을 요약한 것입니다. 즉, 모든 element가 string[] 타입을 가지는 array라고 해석하면 됩니다. let names: string[] = []; // No type errors. let numbers: number[] = []; // No type errors. names.push('Isabella'); numbers.push(30); 이 때, 빈 array []는 어떤 array 타입의 값으로 할당되어도 문제없이 실행됨을 유의합니다. Tuple 자바스크립트의 array는 앞서 말했듯 다양한 타입의 요소들이 섞여서 구성될 수 있습니다. 타입스크립트에서는 이렇게 다양한 타입의 element로 구성된 array를 tuple이라고 부르며, 새로운 자료형으로서 다룹니다. let ourTuple: [string, number, string, boolean] = ['Is', 7 , 'our favorite number?' , false]; Tuple은 위와 같이 []안에 원하는 순서대로 데이터 타입을 나열함으로써 구현합니다. 이로 인해, tuple은 tuple 내 element 타입의 순서와 tuple의 길이가 미리 고정되게 됩니다. let tup: [string, string] = ['hi', 'bye']; let arr: string[] = ['there','there']; tup = ['there', 'there']; // No Errors. tup = arr; // Type Error! An array cannot be assigned to a tuple. 자바스크립트에서는 array나 tuple이 모두 동일하게 간주됩니다. 하지만, 타입스크립트에서는 두 자료형이 다르게 취급되며, 심지어 element들이 동일한 타입을 가졌을지라도 tuple 변수에 array를 할당하는 것이 불가능합니다. Array type inference 타입스크립트는 변수에 초기화된 value 혹은 return statement를 보고 해당 변수의 타입을 추론합니다. 이는 array가 value로 주어져도 마찬가지입니다. let examAnswers= [true, false, false]; 다만, 이때 examAnswers의 타입이 boolean[]인 array인지 [boolean, boolean, boolean]인 tuple인지 헷갈립니다. 타입스크립트는 이에 대해 항상 boolean[] array로서 타입을 추론합니다. 길이 및 타입 순서의 고정 등 제약이 많은 tuple보다 array가 더 자유로운 타입이기 때문입니다. let tup: [number, number, number] = [1,2,3]; let concatResult = tup.concat([4,5,6]); // concatResult has the value [1,2,3,4,5,6]. 위과 같이, tuple과 array를 concatenation하는 상황에서도 concatResult는 array 타입으로 추론됩니다. 따라서, 타입스크립트의 type inference에서는 tuple로 추론되는 경우가 없습니다. Tuple을 사용하고 싶다면, 앞서 확인한 type annotation을 사용해야만 합니다. Rest parameters type annotation function addPower(p: number, ...numsToAdd: number[]): number{ /* rest of function */ } Rest parameters는 고정되지 않은 다수의 인자를 array로서 받습니다. 따라서, array 타입으로서 인자에 기존 방식대로 type annotation을 줄 수 있습니다. Spread syntax with tuple 자바스크립트의 spread syntax는 tuple과 매우 궁합이 잘 맞습니다. function gpsNavigate(startLatitudeDegrees:number, startLatitudeMinutes:number, startNorthOrSouth:string, startLongitudeDegrees: number, startLongitudeMinutes: number, startEastOrWest:string, endLatitudeDegrees:number, endLatitudeMinutes:number , endNorthOrSouth:string, endLongitudeDegrees: number, endLongitudeMinutes: number, endEastOrWest:string) { /* navigation subroutine here */ } 예를 들어, 위의 함수를 두 가지 위치 정보를 받아 경로를 찾는 함수라고 생각해봅시다. 많은 수의 인자를 받는 함수이기 때문에 gpsNavigate(40, 43.2, 'N', 73, 59.8, 'W', 25, 0, 'N', 71, 0, 'W')와 같이 호출하게 됩니다. 이는 가독성이 떨어지기에, 다음과 같이 두 가지 tuple 변수로 나눠봅시다. let codecademyCoordinates: [number, number, string, number, number, string] = [40, 43.2, 'N', 73, 59.8, 'W']; let bermudaTCoordinates: [number, number, string, number, number, string] = [25, 0 , 'N' , 71, 0, 'W']; 그리고 spread syntax를 사용해 조금 더 가독성을 높여 호출해봅시다. gpsNavigate(...codecademyCoordinates, ...bermudaTCoordinates); // And by the way, this makes the return trip really convenient to compute too: gpsNavigate(...bermudaTCoordinates, ...codecademyCoordinates); // If there is a return trip . . . 2. Complex type Enum 관련있는 상수들의 집합을 열거형(Enum)이라고 합니다. 앞서 살펴본 타입들은 해당 타입의 값들이 다양하게 무한한 경우의 수로 존재할 수 있지만, enums는 값의 경우의 수가 제한됩니다. 즉, 내가 원하는 경우의 수 값들로만 타입을 구성하고 enumerate하고 싶을 때 enum이 적합합니다. enum Direction { North, South, East, West } 위와 같이 동서남북을 정의하고 싶을 때, enum을 사용할 수 있습니다. Enum에서 각각의 enum variable(∝key)들에는 number 타입을 가지는 숫자 값(∝value)이 순서대로 자동 대응되게 됩니다. 즉, Direction.North, Direction.South, Direction.East, Direction.West는 각각 0, 1, 2, 3의 값들이 대응됩니다. let whichWayToArcticOcean: Direction; whichWayToArcticOcean = Direction.North; // No type error. whichWayToArcticOcean = Direction.Southeast; // Type error: Southeast is not a valid value for the Direction enum. whichWayToArcticOcean = West; // Wrong syntax, we must use Direction.West instead. Enum으로 만든 custom type은 기존 방식처럼 type annotation을 그대로 사용할 수 있습니다. 그리고 enum으로 type annotation한 변수는 위와 같이 Direction enum에서 .을 통해 접근가능한 값들만 변수에 할당할 수 있게 됩니다. let whichWayToAntarctica: Direction; whichWayToAntarctica = 1; // Valid TypeScript code. whichWayToAntarctica = Direction.South; // Valid, equivalent to the above line. 만일 enum value가 number type이라면, 위의 whichWayToAntarctica = 1; 같이 임의의 enum value를 직접 할당하는 것도 가능합니다. (Enum의 value로는 number 혹은 string 타입 두 가지 경우가 가능합니다. 뒤에서 더 설명하겠습니다.) enum Direction { North = 7, South, East, West } 위와 같이 North가 7부터 시작하는 enum도 만들 수 있습니다. 이 때, South, East, West는 각각 8, 9, 10으로 1씩 자동으로 증가하며 대응됩니다. enum Direction { North = 8, South = 2, East = 6, West = 4 } 만일 모든 값에 각기 다른 값을 대응시키고 싶다면, 위 코드처럼 빠짐없이 명시해주면 됩니다. String Enum VS Numeric Enum Enum은 number 혹은 string 타입에 한해서 자신의 value를 가질 수 있습니다. 앞서 살펴본 enum은 number 타입의 enum value를 가지는 numeric enum이었습니다. 이와 대조적으로, string 타입의 enum value를 가지는 string enum을 살펴봅시다. enum DirectionNumber { North, South, East, West } enum DirectionString { North = 'NORTH', South = 'SOUTH', East = 'EAST', West = 'WEST' } 자동으로 number 타입 값이 할당되던 numeric enum과 달리, string enum은 직접 string 값을 명시해줘야 합니다. String 타입 enum value는 어떤 것이든 올 수 있지만, 관례적으로 enum variable의 대문자 형태를 사용하는 것이 일반적입니다. 덕분에 에러메시지나 로그에 정보가 담기기 때문입니다. 이러한 정보적 측면에서, enum은 항상 string enum으로 사용할 것이 권장됩니다. let whichWayToAntarctica: DirectionString; whichWayToAntarctica = '\ (•◡•) / Arbitrary String \ (•◡•) /'; // Type error! whichWayToAntarctica = 'SOUTH'; // STILL a type error! whichWayToAntarctica = DirectionString.South; // The only allowable way to do this. String enum이 권장되는 또 하나의 이유는 enum 정의 이후에는 임의의 string 값 할당이 불가능함에 있습니다. 위와 같이 enum으로 type annotation된 변수에 string 값을 할당하면, enum에 이미 존재하는 enum value임에도 type error가 발생합니다. 즉, whichWayToAntarctica = DirectionString.South;처럼 오직 .을 통해 접근한 값만 할당 가능합니다. 임의의 값을 마음대로 할당가능한 numeric enum가 대조되는 부분입니다. let whichWayToAntarctica: DirectionNumber; whichWayToAntarctica = 1; // Valid TypeScript code. whichWayToAntarctica = DirectionNumber.South; // Valid, equivalent to the above line. whichWayToAntarctica = 943205; // Also, valid TypeScript code!! Object let aPerson: {name: string, age: number}; Object 역시 type annotation으로 사용할 수 있습니다. 위의 코드는 object type annotation하는 syntax의 예시입니다. Object 리터럴과 매우 비슷해 보이지만, 각 property의 value 위치에 type이 명시되어 있습니다. aPerson = {name: 'Aisle Nevertell', age: "wouldn't you like to know"}; // Type error: age property has the wrong type. aPerson = {name: 'Kushim', yearsOld: 5000}; // Type error: no age property. aPerson = {name: 'User McCodecad', age: 22}; // Valid code. 그리고 실제로 각각의 property의 이름과 type이 동일한 object가 아니라면 타입 에러를 띄웁니다. let aCompany: { companyName: string, boss: {name: string, age: number}, employees: {name: string, age: number}[], employeeOfTheMonth: {name: string, age: number}, moneyEarned: number }; Object의 큰 장점은 property에 type 제한이 없다는 점입니다. Object의 property에는 enum, array 혹은 또 다른 object까지 다양하고 자유롭게 type을 명시할 수 있습니다. Type alias 만일 object나 tuple 타입 같은 복잡한 타입이 동일하게 자주 반복된다면, 해당 타입에 따로 임의의 이름을 정해 효율을 높일 수 있습니다. type <alias name> = <type> 형태로 원하는 타입에 별칭을 부여합니다. let aCompany: { companyName: string, boss: { name: string, age: number }, employees: { name: string, age: number }[], employeeOfTheMonth: { name: string, age: number }, moneyEarned: number }; 위와 같이 반복적으로 동일한 타입이 자주 나오는 경우, 다음과 같이 type alias를 사용해 반복을 줄입니다. type Person = { name: string, age: number }; let aCompany: { companyName: string, boss: Person, employees: Person[], employeeOfTheMonth: Person, moneyEarned: number }; Type alias에서 유의할 점은 type alias는 단순히 별칭일 뿐, 그 자체로는 타입이 아니라는 것입니다. 따라서 아래와 같은 코드는 type alias의 이름이 달라도 내부의 타입은 동일하기 때문에, 타입 에러를 일으키지 않습니다. type MyString = string; type MyOtherString = string; let firstString: MyString = 'test'; let secondString: MyOtherString = firstString; // Valid code. Function type 자바스크립트의 함수는 변수에 담길 수 있습니다. 그리고 타입스크립트는 함수가 담기는 변수에 함수 타입을 적용할 수 있습니다. type StringsToNumberFunction = (arg0: string, arg1: string) => number; 위 코드는 마치 arrow function과 유사하지만, 함수가 아니라 함수 타입을 정의한 것입니다. 위의 문법을 사용해 각각의 파라미터에 타입을 지정할 수 있고, => 뒤에는 return 타입을 지정합니다. 참고로 type StringsToNumberFunction는 type alias에 해당합니다. let myFunc: StringsToNumberFunction; myFunc = function(firstName: string, lastName: string) { return firstName.length + lastName.length; }; myFunc = function(whatever: string, blah: string) { return whatever.length - blah.length; }; // Neither of these assignments results in a type error. 앞서 정의한 함수 타입을 myFunc 변수에 적용했습니다. 이후 myFunc에 할당된 함수들은 모두 StringsToNumberFunction 타입에 어긋나지 않기 때문에, 타입에러가 일어나지 않습니다. 이 때, 함수 타입에 설정한 arg0, arg1과 실제 파라미터의 이름은 달라도 괜찮습니다. type StringToNumberFunction = (string)=>number; // NO type StringToNumberFunction = arg: string=>number; // NO NO NO NO 함수 타입에서 유의할 점은 파라미터의 이름을 안 쓴다거나 파라미터를 둘러싸는 ()를 빼먹어서는 안되는 것입니다. 함수 타입에서는 파라미터가 하나여도 ()를 반드시 써줘야 합니다. type OperatorFunction = (arg0: number, arg1: number) => number; // Math Tutor Function That Accepts a Callback function func(operationCallback: OperatorFunction) { operationCallback(); } 특히, 콜백 함수를 인자로 받을 때 이러한 함수 타입들은 더욱 유용할 것입니다. Generic type 제네릭(Generic)은 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미합니다. 예를 들어, 클래스를 정의 할 데이터 타입을 확정하지 않고 인스턴스를 생성할 때 데이터 타입을 지정하는 것은 제네릭에 한 예입니다. 앞서 봤던, array element에 타입을 적용하는 Array<T> 문법도 제네릭의 예시에 해당합니다. type Family<T> = { parents: [T, T], mate: T, children: T[] }; Object에도 위와 같이 제네릭을 적용할 수 있습니다. Family<T> 자체로는 아직 type annotation을 할 수 없습니다. T라는 타입 변수 자리에 원하는 타입을 대체했을 때, 비로소 type annotation으로 기능할 수 있습니다. 이 때, 식별자 T는 관습적으로 쓰이는 것이므로 임의로 바꿔도 괜찮습니다. let aStringFamily: Family<string> = { parents: ['stern string', 'nice string'], mate: 'string next door', children: ['stringy', 'stringo', 'stringina', 'stringolio'] }; 위와 같이 타입 변수 T를 string으로 대체하면, 타입 내 원래의 T 자리는 모두 string으로 대체되기 때문에, 위 코드는 결과적으로 에러 없이 잘 동작합니다. Generic function 함수의 리턴 타입을 설정할 때도 제네릭은 유용하게 사용됩니다. function getFilledArray<T>(value: T, n: number): T[] { return Array(n).fill(value); } 위와 같이 array를 생성하는 함수는 array에 삽입되는 요소의 타입이 value의 값에 따라 달라지기 때문에, 리턴되는 array의 타입을 정의하기가 어렵습니다. 따라서, 제네릭을 적용하여 T로 리턴 타입 정의를 용이하게 합니다. getFilledArray<string>('cheese', 3) 제네릭 함수의 호출은 위와 같이 원하는 타입을 <> 안에 명시하여 실행합니다. getFilledArray<string>의 경우는 결과적으로 type annotation (value: string, n: number): string[]와 동일해집니다. 즉, T가 함수 내부의 type annotation에서 적용 가능해집니다. Reference Codecademy - TypeScript
JavaScript-Ecosystem
· 2021-10-13
Next.js basic - Pre-rendering
Pre-rendering Pre-rendering은 Next.js의 중요한 특징 중 하나입니다. Next.js는 클라이언트에서 HTML 생성을 모두 처리하기 보다는, 처음에 모든 페이지에 대한 HTML을 미리 생성하는데, 이것을 pre-rendering이라고 합니다. Pre-rendering 덕분에 Next.js는 SEO와 더불어 좋은 성능을 보입니다. Pre-rendering 이후에는 hydration이라는 과정을 거칩니다. Hydration이란 브라우저가 페이지를 로딩할 때, 해당 페이지를 로딩하기 위해 필요한 최소한의 자바스크립트 코드만을 가져와 실행시켜서 미리 생성되어 있는 HTML을 interactive하게 만드는 과정을 말합니다. 만일 순수 리액트 코드로 작성된 애플리케이션의 경우, pre-rendering이 없기 때문에 페이지들의 HTML을 미리 생성하지 않습니다. 그래서 만일 순수 리액트 코드로 이루어진 애플리케이션의 자바스크립트 코드를 disabled 상태로 만든다면, 페이지 자체가 보이지 않게 됩니다. 반면, Next.js가 적용된 애플리케이션은 static HTML이 미리 생성된 덕분에 자바스크립트 기능을 제외한 페이지 자체는 보이게 됩니다. Two forms of pre-rendering Next.js의 pre-rendering은 Static Generation과 Server-side Rendering이라는 두 가지 형태가 존재합니다. 두 형태의 차이점은 언제 페이지에 대한 HTML이 생성되는가에 있습니다. 먼저, Static Generation은 build-time에 HTML을 생성하는 pre-rendering method입니다. 즉, 클라이언트의 request 이전에 HTML이 생성됩니다. 이렇게 pre-rendering된 HTML은 각각의 request에 요청될 때마다 재사용됩니다. Static Generation은 request에 상관없이 내용이 자주 바뀌지 않는 marketing page, blog post, E-commerce product listing, documentation 등에 유용합니다 이와 달리, Server-side Rendering은 각각의 request가 올 때마다 HTML을 생성하는 pre-rendering method입니다. 즉, 클라이언트의 request 후에 HTML이 생성되며, 생성된 HTML은 재사용되지 않습니다. Server-side Rendering은 빈번히 update되는 데이터 혹은 request마다 content가 바뀜으로 인해, request 이전에 pre-render하기 어려운 상황에서 유용합니다. 참고로, 개발자 모드로 서버를 실행했을 때는 모든 페이지가 Server-side Rendering으로 pre-rendering됩니다. 심지어 Static Generation을 사용하는 페이지라고 하더라도 마찬가지입니다. Next.js는 각각의 페이지마다 위의 두 가지 형태 중 어떤 pre-rendering을 사용할지 선택할 수 있습니다. 따라서, Static Generation과 Server-side Rendering 방식이 혼합된 Next.js 애플리케이션을 만들 수 있습니다. 다만, 대부분의 경우에서는 Static Generation이 권장됩니다. 매 request 마다 HTML을 생성해야 하는 Server-side rendering에 비해, 한 번 HTML을 생성하고 재사용하는 Static Generation이 훨씬 빠르기 때문입니다. 만일 항상 최신 상태를 유지해야 하는 데이터를 처리할 경우, 느림을 감안하고서라도 Server-side Rendering을 사용하거나 pre-rendering을 생략하고 Client-side Rendering을 사용하는 것이 적합합니다. Static Generation with and without data Static Generation은 외부 데이터가 필요할 때 혹은 필요하지 않을 때 모두 사용할 수 있습니다. 외부적으로 데이터를 가져오지 않아도 되는 페이지들은 자동으로 Static Generation될 것입니다. 이와 달리, 처음에 반드시 데이터를 가져와야 하는 페이지의 경우, 빌드 시간에 파일 시스템에 접근하거나 외부 API 혹은 데이터베이스 등에 request를 해야만 합니다. 이러한 요청은 getStaticProps을 사용해 진행합니다. export default function Home(props) { ... } export async function getStaticProps() { // Get external data from the file system, API, DB, etc. const data = ... // The value of the `props` key will be // passed to the `Home` component return { props: ... } } 비동기 함수 getStaticProps 안에서 실행하는 모든 것들은 빌드 타임에 진행되고, 요청을 통해 응답받은 데이터는 props의 형태로 데이터가 필요한 페이지 컴포넌트에 전달할 수 있습니다. 또한, getStaticProps 함수 내에 원하는 로직을 완성했다면, 페이지 컴포넌트를 export한 것과 마찬가지로 getStaticProps 함수도 export해주는 것을 유의해야 합니다. getStaticProps에 대한 몇 가지 유의할 점 getStaticProps 함수는 항상 server-side에서 실행됩니다. 즉, 브라우저에서 실행될 염려가 없기 때문에, 필요한 데이터를 가져오기 위해 데이터베이스에 쿼리를 날리는 것 역시 문제가 되지 않습니다. 개발자 모드로 서버를 실행했다면, getStaticProps는 request가 있을 때마다 실행되는 Sever-side Rendering 방식으로 동작합니다. (npm run dev, yarn dev) 반면에, production용으로 서버가 실행되었다면, getStaticProps 함수는 원래 의도대로 빌드 시간에 실행됩니다. getStaticProps 함수는 항상 page 파일에서 export되어야 합니다. Non-page 파일에서 export 되어서는 안됩니다. Server-side Rendering 만일 Server-side Rendering을 하고 싶다면, getServerProps를 사용합니다. CDN에 캐시되지 않아 getStaticProps보다는 느리겠지만, 최신의 정보를 request 때마다 가져올 수 있습니다. export async function getServerSideProps(context) { return { props: { // props for your component } } } context 매개변수를 사용하면, request와 관련된 매개변수들을 다룰 수 있습니다. Client-side Rendering 만일 데이터 pre-rendering을 생략하고 싶다면, Client-side Rendering을 사용합니다. 페이지에서 외부적으로 데이터를 필요로하지 않는 부분만 Static Generation하고, 페이지의 나머지 부분은 client에서 자바스크립트를 사용해 데이터를 가져오면서 로딩할 수 있습니다. (Static Generation without data + Fetch data on the Client-Side) 만일 client-side에서 데이터를 fetching하고 싶다면, Next.js에서 제공하는 리액트 훅 SWR을 사용할 것을 권장합니다. 다음은 SWR의 예시입니다. import useSWR from 'swr' function Profile() { const { data, error } = useSWR('/api/user', fetch) if (error) return <div>failed to load</div> if (!data) return <div>loading...</div> return <div>hello {data.name}!</div> } Reference Next.js Document
JavaScript-Ecosystem
· 2021-10-13
TypeScript basic - Function
Parameter type annotation 변수에 type annotation을 했던 것처럼, 타입스크립트는 함수의 파라미터에 type annotation을 하여 파라미터가 원하는 데이터 타입을 가지도록 할 수 있습니다. 물론, 기존 자바스크립트에서도 파라미터의 타입을 validation할 수 있는 방법이 있습니다. function printLengthOfText(text) { if (typeof text !== 'string') { throw new Error('Argument is not a string!'); } console.log(text.length); } printLengthOfText(3); // Error: Argument is not a string! 다만 조건문을 만들고 error를 일으키는 작업이 조금 번거롭습니다. 타입스크립트는 이러한 불편함을 type annotation을 사용해 다음과 같이 간단히 해결합니다. function printKeyValue(key: string, value) { console.log(`${key}: ${value}`); } printKeyValue('Courage', 1337); // Prints: Courage: 1337 printKeyValue('Mood', 'scared'); // Prints: Mood: scared 이로 인해, key는 string 타입을 가져야 하며, annotation이 없는 value는 any 타입을 부여받게 됩니다. Optional parameter function greet(name: string) { console.log(`Hello, ${name || 'Anonymous'}!`); } greet('Anders'); // Prints: Hello, Anders! greet(); // TypeScript Error: Expected 1 arguments, but got 0. JavaScript는 인자 없이 greet()을 실행했을 때, name은 undefined가 되고 이는 falsy value로 인식되어 결국 ‘Hello, Anonymous`가 콘솔에 출력될 것입니다. 그러나 타입스크립트는 optional을 따로 지정해주지 않으면 이에 대하여 오류를 일으킵니다. 따라서 optional 파라미터를 사용하고 싶다면, 다음과 같이 파라미터 뒤에 ?를 사용해 해당 파라미터가 optional 함을 선언해줍니다. function greet(name?: string) { console.log(`Hello, ${name|| 'Anonymous'}!`); } greet(); // Prints: Hello, Anonymous! Default parameter 파리미터의 기본값을 지정해주면 해당 파리미터는 optional해지며 동시에 기본값의 타입과 동일한 타입의 데이터가 인자로 올 것이 전제됩니다. function greet(name = 'Anonymous') { console.log(`Hello, ${name}!`); } 위 코드에 대해 인자없이 greet()을 실행하면, ‘Hello, Anonymous!’를 출력합니다. 반면에, greet(3)과 같이 number 값을 인자로 전달하면 타입 에러를 야기합니다. 이는 name의 인자로 string 혹은 undefined 값이 올 것이라고 파라미터의 default 값으로 인해 설정되었기 때문입니다. Inferring return type 타입스크립트는 함수의 리턴 값의 타입 역시 추론합니다. function ouncesToCups(ounces: number) { return `${ounces / 16} cups`; } const liquidAmount: number = ouncesToCups(3); // Type 'string' is not assignable to type 'number'. 예를 들어, ouncesToCups 함수는 return statement의 값이 string이므로, string 값을 반환할 것이 분명히 예측됩니다. 따라서 liquidAmount 역시 string 값이 되어야 하는데 number로 변수를 선언했으므로 타입 에러가 나타납니다. Return type annotation 또한, type annotation을 사용하면 함수의 리턴 값에 대해서도 더 분명하게 타입 선언을 해줄 수 있습니다. function createGreeting(name?: string): string { if (name) { return `Hello, ${name}!`; } return undefined; //Typescript Error: Type 'undefined' is not assignable to type 'string'. }; 함수의 () 바로 뒤에 : type을 설정하면, 함수의 반환 값의 타입을 지정해줄 수 있습니다. 뿐만 아니라 Arrow function에도 마찬가지로 리턴 값에 대한 타입을 지정해줄 수 있습니다. const createArrowGreeting = (name?: string): string => { if (name) { return `Hello, ${name}!`; } return undefined; // Typescript Error: Type 'undefined' is not assignable to type 'string'. }; Void return type 함수에 특별한 이유가 없는 한, return type을 type annotation으로 명시해주는 것이 좋은 습관입니다. 다만, 따로 리턴하는 것이 없는 함수에 대해서는 void를 사용해 type annotation을 해주는 것이 적절합니다. function logGreeting(name:string): void{ console.log(`Hello, ${name}!`) } Documentation comments /** * This is a documentation comment */ 함수에 대한 설명을 등록하고 마우스 호버 등을 통해 이를 확인하고 싶다면 documentation comments 기능을 활용합니다. /** * Returns the sum of two numbers. * * @param x - The first input number * @param y - The second input number * @returns The sum of `x` and `y` * */ function getSum(x: number, y: number): number { return x + y; } } 위와 같이, 원하는 함수 위에 documentation comment를 등록하면 함수에 대한 설명을 입력할 수 있습니다. 또한, @param, @returns 등의 special tags를 활용하면, 함수의 특정 요소를 강조하는 comment를 입력할 수 있습니다. Reference Codecademy - TypeScript
JavaScript-Ecosystem
· 2021-10-12
Next.js basic - Asset, Metadata and CSS
CSS, assets and metadata Next.js에서는 CSS를 어떻게 적용하여 스타일링할 수 있을까요? 그리고 이미지와 같은 정적 파일들과 <title>과 같은 페이지 내 메타 데이터들은 Next.js에서 어떻게 다뤄야 할까요? Asset with <Image> and image optimization 이미지와 같은 정적 파일들은 public 디렉토리에 위치시킵니다. Next.js는 public에 있는 파일들을 자동으로 참조합니다. 이미지를 저장하기 위해 public 디렉토리에 images 디렉토리를 생성하고, 그 안에 원하는 이미지를 저장하세요. (예를 들어, 프로필 사진을 사용하기 위해 profile.jpg를 저장해보세요.) import Image from 'next/image' const YourComponent = () => ( <Image src="/images/profile.jpg" // Route of the image file height={144} // Desired size with correct aspect ratio width={144} // Desired size with correct aspect ratio alt="Your Name" /> ) 저장한 이미지는 Image 컴포넌트를 next/image에서 임포트해 사용합니다. height와 width 속성을 사용해 이미지의 렌더링 사이즈를 지정해주고, src로 이미지의 위치를 설정해줍니다. 기존 HTML <img> 태그는 브라우저의 화면 크기가 바뀔 때마다 변화에 대한 이미지의 resizing을 지원하지 않습니다. 반면에, Next.js의 <Image> 컴포넌트를 사용하면, 해당 이미지의 resizing을 자동으로 지원해줍니다. 또한, <Image> 컴포넌트는 이미지의 포멧도 브라우저에서 WepP와 더 나은 이미지 포멧을 지원한다면, 자동으로 포멧을 변환해서 이미지 파일을 optimization해줍니다. 뿐만 아니라, 애플리케이션의 빌드 타임에서 이미지를 로딩하는 대신, 이미지가 viewport에 나올 때 비로소 lazy-loading하여, 페이지 전체 로딩 시간을 원활히 합니다. Metadata 페이지의 메타 데이터를 변경하고 싶다면, Next.js의 <Head> 컴포넌트를 사용합니다. HTML <head>와는 달리, <Head>는 리액트 컴포넌트입니다. import Head from 'next/head' <Head> 컴포넌트는 'next/head'에서 임포트합니다. export default function FirstPost() { return ( <> <Head> <title>First Post</title> </Head> <h1>First Post</h1> <h2> <Link href="/"> <a>Back to home</a> </Link> </h2> </> ) } 그리고 원하는 메타 데이터를 <Head> 컴포넌트 안에서 설정해줍니다. 위 코드는 페이지의 <title> 속성을 변경했습니다. 개발자 도구에서 해당 페이지의 HTML 문서를 확인해보면, 실제로 <head>에 <title> 태그가 추가되어 있는 것을 볼 수 있습니다. CSS styling <style jsx>{` … `}</style> Next.js에서 CSS는 <style jsx> 태그에 작성하면 됩니다. <style jsx>는 styled-jsx 라이브러리를 사용해 지원되는 것이며, Next.js는 built-in으로 제공됩니다. CSS와 Sass 역시 마찬가지로 built-in으로 지원됩니다. Layout component & CSS module CSS 스타일을 적용하기 위해, Layout 컴포넌트와 CSS module을 사용해봅시다. 먼저 최상위 디렉토리에 components 디렉토리를 하나 생성합니다. export default function Layout({ children }) { return <div>{children}</div> } 그리고 components/layout.js를 생성하여 위와 같은 Layout 컴포넌트를 작성합니다. 이 Layout 컴포넌트는 모든 페이지에 걸쳐 사용될 것입니다. import Head from 'next/head' import Link from 'next/link' import Layout from '../../components/layout' export default function FirstPost() { return ( <Layout> <Head> <title>First Post</title> </Head> <h1>First Post</h1> <h2> <Link href="/"> <a>Back to home</a> </Link> </h2> </Layout> ) } 그리고 CSS를 추가하고 싶은 페이지에 <Layout> 컴포넌트를 감싸서 적용해줍니다. .container { max-width: 36rem; padding: 0 1rem; margin: 3rem auto 6rem; } <Layout>에 적용해줄 CSS는 CSS Module을 사용해 생성합니다. CSS Module은 CSS 파일을 임포트해 리액트 컴포넌트에서 사용하는 것을 도와줄 것입니다. components/layout.module.css 파일을 생성하여, 위와 같이 원하는 CSS 코드를 작성합니다. 특히, CSS Modules를 사용하기 위해서는 생성한 CSS 파일의 이름이 반드시 .module.css로 끝나야함을 유의합니다. import styles from './layout.module.css' export default function Layout({ children }) { return <div className={styles.container}>{children}</div> } 끝으로, Layout 컴포넌트에 CSS를 적용합니다. layout.module.css 파일을 임의의 이름에 임포트해 사용합니다. 여기서는 styles를 사용합니다. 그리고 Layout 내에서 className 속성을 사용해 styles.container를 적용합니다. 이 후, http://localhost:3000/posts/first-post 페이지에 들어가보면, CSS가 잘 적용된 것을 확인할 수 있습니다. Unique class name의 자동 생성 CSS가 적용된 해당 페이지에서 개발자 도구를 열어 HTML 문서를 확인해보면, Layout 컴포넌트로 인해 렌더링된 다음과 같은 class name으로 새로운 <div>가 생성되어 있는 것을 볼 수 있습니다. <div class="layout_container__2t4v2"> 이는 CSS Module이 자동으로 생성한 고유한 class name입니다. 뒷 부분의 고유 문자열 덕분에 class name이 충돌할 여지는 없습니다. 또한, Next.js의 code splitting은 CSS Module에서도 적용되어, 현재 페이지가 로딩될 때 필요한 최소한의 CSS만 함께 로딩되게 됩니다. Global CSS 만일 모든 페이지에서 항상 적용 및 로딩되는 CSS를 원한다면, pages/_app.js 파일을 생성하고 _app.js 파일 내부에서 해당 CSS 파일을 임포트하면 됩니다. export default function App({ Component, pageProps }) { return <Component {...pageProps} /> } 먼저, pages/_app.js 파일을 생성하고 파일 내부에 위 컴포넌트를 작성합니다. App 컴포넌트는 가장 최상위 컴포넌트로서 모든 페이지에 영향을 줍니다. 특히, 페이지들 간의 이동이 있을 때, App 컴포넌트에 state을 저장해두면 유용합니다. 그리고 npm run dev로 서버를 다시 실행해줍니다. _app.js를 추가했을 때는 항상 서버를 다시 실행해줘야 변경사항이 저장됨을 유의합니다! html, body { padding: 0; margin: 0; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; line-height: 1.6; font-size: 18px; } * { box-sizing: border-box; } a { color: #0070f3; text-decoration: none; } a:hover { text-decoration: underline; } img { max-width: 100%; display: block; } 그리고 최상위 디렉토리 밑에 styles 디렉토리를 하나 만들어, 위와 같이 원하는 CSS 코드를 styles/global.css로 파일을 생성해 저장합니다. import '../styles/global.css' export default function App({ Component, pageProps }) { return <Component {...pageProps} /> } 그리고 pages/_app.js에서 global.css를 임포트해주면, 페이지를 이동해도 global.css의 내용이 항상 적용되는 것을 확인할 수 있습니다. 여기서 주의할 점은 global.css는 항상 _app.js 내에서 임포트해줘야 한다는 것입니다. global.css는 항상 모든 페이지에 영향을 주어야 하기 때문입니다. Reference Next.js Document
JavaScript-Ecosystem
· 2021-10-12
TypeScript basic - Type
TypeScript 타입스크립트는 2012년 마이크로소프트가 발표한 기존 자바스크립트에 정적 타입 문법을 추가한 프로그래밍 언어입니다. 자바스크립트의 슈퍼셋(Superset)이기 때문에 타입스크립트 컴파일러 혹은 바벨(Babel)을 이용해 자바스크립트 코드로 변환되어 실행됩니다. 동적 타입의 인터프리터 언어인 자바스크립트와 달리, 타입스크립트는 정적 타입의 컴파일 언어이며 미리 타입을 결정하기 때문에 실행 속도가 매우 빠릅니다. 다만, 매 코드 작성시 타입을 설정하는 번거로움과 더불어, 늘어가는 코드량으로 인해 컴파일 속도는 오래걸린다는 단점이 함께 합니다. 그러나 타입스크립트의 가장 큰 장점은 코드 작성단계에서 타입을 체크해 에러를 사전에 방지할 수 있다는 점입니다. 또한, IDE의 코드 자동 완성을 지원하기 때문에 개발 생산성을 크게 향상시키는 이점도 있습니다. 타입 추론(Type inference) 타입 추론(Type inference)은 타입스크립트가 변수의 데이터 타입을 정할 때 처음 정의할 때 할당한 값을 분석하여 타입을 추론해 지정하는 방식입니다. let order = 'first'; order = 1; 따라서 위와 같이 처음 order를 정의할 때 String 값으로 정의했다면, order에는 1과 같은 Number 타입의 값이 재할당될 수 없습니다. "MY".toLowercase(); // Property 'toLowercase' does not exist on type '"MY"'. // Did you mean 'toLowerCase'? 또한, 타입스크립트는 유추한 해당 타입의 shape 역시 확인하여 위와 같이 메서드 이름 오타로 인한 버그도 쉽게 잡아낼 수 있습니다. Any 만일 변수에 값을 할당하지 않고 선언만 한다면, 해당 변수는 any 타입을 가집니다. let onOrOff; onOrOff = 1; onOrOff = false; 위의 onOrOff는 선언만 되었기 때문에, any 타입을 가집니다. 이 경우, 변수의 값이 기존과 다른 타입의 값으로 재할당되어도 타입스크립트는 오류를 일으키지 않습니다. Type annotation 변수에 값을 할당하지 않고 선언만 했을 때, 해당 변수가 any 타입이 아니라 특정 타입을 명확히 가지길 원할 수 있습니다. 이 때, type annotation을 사용합니다. let mustBeAString : string; mustBeAString = 'Catdog'; mustBeAString = 1337; // Error: Type 'number' is not assignable to type 'string' 위와 같이, let mustBeAString : string;으로 String 타입을 명확히 지정해두면, String 이외 타입의 원치 않는 데이터 할당을 막을 수 있습니다. Reference Codecademy - TypeScript 타입스크립트 핸드북 활용도가 높아지는 웹 프론트엔드 언어, 타입스크립트(TypeScript)
JavaScript-Ecosystem
· 2021-10-11
Next.js basic - Pages
페이지 이동 구현 방법 Next.js를 사용해 여러 개의 페이지를 만들고 이동하는 방법을 소개합니다. Next.js는 code splitting, client-side navigation, prefetching 등을 통해, 자동으로 애플리케이션의 성능을 best performance로 최적화합니다. 페이지 만들기 먼저 새로운 페이지를 만들어봅시다. Next.js에는 pages 디렉토리가 존재합니다. 해당 디렉토리에 원하는 URL로 js 파일을 생성하면, 쉽게 새로운 페이지를 만들 수 있습니다. 예를 들어, pages/posts/first-post.js라는 경로로 새로운 페이지를 만들었다면, 해당 페이지의 URL은 /posts/first-post이 됩니다. export default function FirstPost() { return <h1>First Post</h1> } 그리고 위와 같이 컴포넌트를 만들고 서버를 실행하면, http://localhost:3000/posts/first-post에 해당 페이지가 뜨게 됩니다. 이 때, 컴포넌트는 항상 default export가 되어야 함을 유의합니다. 이러한 방식은 HTML과 PHP를 사용하여 웹사이트를 구축하는 방식과 비슷하지만, HTML 대신에 JSX와 React component를 사용했다는 점이 다릅니다. 이제 남은 것은 홈페이지에 새로운 페이지로 가는 링크만 걸어주는 것입니다! Link component Next.js에서 페이지의 링크를 걸어주는 것은 <Link> 컴포넌트를 사용해서 수행합니다. import Link from 'next/link' 이를 위해, 먼저 'next/link'로부터 Link 컴포넌트를 import합니다. <h1 className="title"> Read{' '} <Link href="/posts/first-post"> <a>this page!</a> </Link> </h1> 그리고 index.js에서 위와 같이 코드를 작성하면, 새로 만든 페이지의 URL /posts/first-post로 이동하는 링크를 만들 수 있습니다. 여기서 <Link>가 <a> 태그를 감쌌다는 점, href 속성은 <Link> 태그에 주었다는 점을 유의합니다. 참고로, {' '}은 multiple line text를 나누기 위해 사용됩니다. import Link from 'next/link' export default function FirstPost() { return ( <> <h1>First Post</h1> <h2> <Link href="/"> <a>Back to home</a> </Link> </h2> </> ) } 앞선 pages/posts/first-post.js에도 위와 같이 홈으로 돌아가는 링크를 만들면, 페이지끼리 서로 이동할 수 있게 됩니다. Link and client-side navigation <Link>의 사용은 client-side navigation을 가능하게 합니다. 즉, 페이지 전환이 클라이언트 측에서 자바스크립트를 이용해 일어나기 때문에, 페이지의 모든 부분을 서버에서부터 새로 가져와 로딩하는 브라우저 기본 navigation 방식보다 훨씬 빠르게 동작합니다. 만일 <Link>가 아닌 <a> 태그를 사용했다면, 브라우저는 해당 링크에 접근할 때마다 페이지 전체를 refresh할 것입니다. Code splitting Next.js에서는 code splitting이 자동적으로 일어나므로, 페이지의 로딩도 해당 페이지에 반드시 필요한 것들만 로딩됩니다. 예를 들어, 홈페이지가 렌더링될 때는 다른 페이지들은 로딩되지 않습니다. 특히, 애플리케이션에 수 많은 페이지가 있을 때, 유저는 자신이 요청한 페이지를 보다 빠르게 볼 수 있게 됩니다. 즉, 페이지들의 코드는 각각 분리되어 있고, 어떤 특정 페이지가 오류를 일으켜도 애플리케이션의 나머지 부분은 문제없이 동작합니다. Prefetching 브라우저의 viewport(메뉴 바, 탭 영역을 제외한 브라우저의 순수 화면 영역)에 <Link> 컴포넌트가 있을 때마다, Next.js는 <Link>에 연결된 페이지들을 자동으로 미리 로딩해둡니다. 이를 prefetching이라고 하며, 이러한 페이지들은 유저가 링크를 누를 때 background에서 이미 로딩되어 있어서 매우 빠르게 페이지가 전환됩니다. Reference Next.js Document
JavaScript-Ecosystem
· 2021-10-11
React - Advanced tips
Programming patterns 리액트는 자주 사용되는 프로그래밍 패턴이 존재합니다. Scene 1 - Stateful components to stateless components Stateful component가 자신의 state setter 함수를 props로 child component에 전달하면, child component의 어떠한 event에 의해 해당 함수가 호출되어 parent component의 state를 변경합니다. 그리고 parent component는 변경된 state를 props로 또 다른 child component(=sibling component)에게 전달해 해당 child component에서 화면에 표시합니다. Scene 2 - Separating container components from presentational components State를 가지거나 calculation 등의 functional part를 담당하는 component는 container component로, 렌더링을 담당하는 component는 presentational component로 분리해야 합니다. 분리된 presentational component는 항상 container component에 의해서 렌더링되어야 합니다. Style Name Syntax 일반적인 JavaScript에서 style의 name은 hyphenated-lowercase로 이루어져 있습니다. const styles = { 'margin-top': '20px', 'background-color': 'green' }; 반면에, 리액트는 style name이 camelCase로 이루어져 있습니다. const styles = { marginTop: '20px', backgroundColor: 'green' }; Style Value Syntax 일반적인 JavaScript에서는 "450px", "20%" 처럼 숫자와 단위를 함께 적어 string 형태로 style value를 사용해야 합니다. 하지만, 리액트에서는 px에 한해서 생략이 가능하고, 이 경우 숫자도 string이 아닌 number 그대로 사용하는 것이 가능합니다. 물론 기존의 string 형태도 그대로 사용 가능합니다. { fontSize: 30 } 다만, 다른 단위를 사용하고 싶을 때는 기존의 string 형태로 사용합니다. { fontSize: "2em" } propTypes propTypes는 리액트에서 자주 사용되는 특징입니다. Prop이 전달될 것이 예상되는 component에 올바른 prop이 전달되었는지에 대한 validation을 도와주고, documentation을 통해 component의 상황을 한눈에 파악할 수 있도록 도와줍니다. import PropTypes from 'prop-types'; propTypes를 사용하기 위해선 'prop-types' 라이브러리를 import해야 합니다. import React from 'react'; import PropTypes from 'prop-types'; export class MessageDisplayer extends React.Component { render() { return <h1>{this.props.message}</h1>; } } // This propTypes object should have // one property for each expected prop: MessageDisplayer.propTypes = { message: PropTypes.string }; 그리고 미리 정의된 component에 위와 같이 property를 추가하는 방식으로 propTypes를 정의할 수 있습니다. 이 때, propTypes의 value는 object 형태여야 함을 유의합니다. 그리고 해당 object의 각각의 property는 component에 전달될 것이 기대되는 prop의 이름으로 설정합니다. Runner.propTypes = { message: PropTypes.string.isRequired, style: PropTypes.object.isRequired, isMetric: PropTypes.bool.isRequired, miles: PropTypes.number.isRequired, milesToKM: PropTypes.func.isRequired, races: PropTypes.array.isRequired }; PropTypes를 통해 설정할 수 있는 data type의 이름은 위와 같습니다. isRequired의 경우, prop이 잘 전달되는지 확인해서 만일 잘 전달되지 않으면 console에 warning을 띄어주는 역할을 합니다. const Example = (props) => { return <h1>{props.message}</h1>; } Example.propTypes = { message: PropTypes.string.isRequired }; 만일 function component에 propTypes를 추가하고 싶다면, 위와 같이 function component 자체의 property로 propTypes를 지정합니다. React forms import React from 'react'; import ReactDOM from 'react-dom'; export class Input extends React.Component { constructor(props) { super(props); this.state = { userInput: '' }; this.handleUserInput = this.handleUserInput.bind(this); } handleUserInput(e) { this.setState({userInput: e.target.value}); } render() { return ( <div> <input type="text" value={this.state.userInput} onChange={this.handleUserInput} /> <h1>{this.state.userInput}</h1> </div> ); } } ReactDOM.render( <Input />, document.getElementById('app') ); 일반적인 form은 유저가 input field에 계속 타이핑하더라도 submit 버튼을 누르기전까지는 서버에서 그 사실을 알지 못합니다. 즉, submit 이전까지 프론트가 알고 있는 input 정보와 서버가 알고 있는 input 정보 사이에 불일치가 존재합니다. 그러나 이러한 불일치는 웹사이트의 third part에서 해당 정보를 필요로 할 때, 프론트냐 서버냐에 따라 다른 결과를 내어 문제가 발생할 수 있습니다. 이를 해결하기 위해, 리액트 form은 모든 new character와 deletion에 대한 프론트 및 서버의 동기화를 지원하여 application의 모든 요소가 일관성 있게 동작하도록 합니다. 특히, 일반적인 <form> tag를 굳이 사용하지 않고 위 코드처럼 <input> tag만으로 이를 구현할 수 있습니다. Uncontrolled vs Controlled component Uncontrolled component란 스스로 state를 가지고 그 값을 기억하는 component를 말합니다. 반면에 controlled component는 스스로 state를 가지지 않고 다른 component에 의해 관리되어지는 component를 말합니다. 리액트에는 주로 controlled component가 많고 이러한 component는 스스로에 대한 정보를 props를 통해 얻게 됩니다. Reference Learn React - Codecademy
JavaScript-Ecosystem
· 2021-08-30
React - Hook
Functional components 지금까지 JavaScript의 클래스를 사용해서 정의한 리액트의 component들은 함수를 사용해서 정의할 수도 있습니다. 이를 function component라고 합니다. Function component는 간단하고 직관적이라는 장점이 있습니다. // A component class written in the usual way: class MyComponentClass extends React.Component { render() { return <h1>Hello world</h1>; } } // The same component class, written as a stateless functional component: const MyComponentClass = () => { return <h1>Hello world</h1>; } // Works the same either way: ReactDOM.render( <MyComponentClass />, document.getElementById('app') ); Function component는 위와 같이 함수 형태로 작성하며, render() 메서드를 사용하지 않고 JSX expression을 바로 리턴하는 방식으로 작성합니다. Function component는 props 역시 전달받을 수 있습니다. function WelshCorgi (props) { return ( <div> <p>{props.prompt}</p> </div> ); } ReactDOM.render( <WelshCorgi feed="High quality dog feed" />, document.getElementById('app'); ); props는 parameter로 정의해 전달받고, props.propertyName 형식으로 접근합니다. Hook Hook은 function component에서 component의 state와 이후의 렌더링 관련 side effects를 관리하도록 도와주는 함수들입니다. 클래스에서는 작동되지 않지만, function component에서 lifecycle적인 특징들도 관리할 수 있도록 도와줍니다. State hook - useState import React, { useState } from "react"; function Toggle() { const [toggle, setToggle] = useState('off'); return ( <div> <p>The toggle is {toggle}</p> <button onClick={() => setToggle("On")}>On</button> <button onClick={() => setToggle("Off")}>Off</button> </div> ); } useState는 리액트 라이브러리에서 제공하는 JavaScript 함수로, 호출 시 두 가지 value가 담긴 array를 리턴합니다. current state - the current value of this state state setter - a function that we can use to update the value of this state State에 대한 초깃값은 useState에 인자로 넣어진 값으로 설정할 수 있습니다. 초깃값이 중요하지 않은 경우, 인자를 넣지 않고 초깃값을 undefined 상태로 두어도 상관없으나 null 값이라도 넘겨주는 것이 가독성을 높이는 방법이 될 수 있습니다. useState를 사용해서 임의의 value를 인자로 state setter 함수를 호출하면, 현재 state를 새로운 state로 update할 수 있습니다. 특히 state setter 함수가 호출되면 리액트는 자동으로 해당 component를 다시 렌더링하므로 변경한 새로운 state value가 바로 반영됩니다. import React, { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); const increment = () => setCount(prevCount => prevCount + 1); return ( <div> <p>Wow, you've clicked that button: {count} times</p> <button onClick={increment}>Click here!</button> </div> ); } 만일 기존의 state를 활용해 계산한 값으로 state를 update하고 싶다면, state setter 함수에 콜백 함수를 인자로 전달하면 됩니다. 위와 같이 기존 state count를 활용해 prevCount + 1 값으로 state를 update하고 싶다면, setCount(prevCount => prevCount + 1)처럼 콜백 함수를 state setter 함수의 인자로 넣어줍니다. 특정한 상황에서는 setCount(count +1) 같이 바로 값을 update할 수도 있지만, 콜백 함수를 사용하는 방법이 모든 상황에서 더 안전하다는 점을 유의합니다. import React, { useState } from "react"; const options = ["Bell Pepper", "Sausage", "Pepperoni", "Pineapple"]; export default function PersonalPizza() { const [selected, setSelected] = useState([]); const toggleTopping = ({target}) => { const clickedTopping = target.value; setSelected((prev) => { // check if clicked topping is already selected if (prev.includes(clickedTopping)) { // filter the clicked topping out of state return prev.filter(t => t !== clickedTopping); } else { // add the clicked topping to our state return [clickedTopping, ...prev]; } }); }; return ( <div> {options.map(option => ( <button value={option} onClick={toggleTopping} key={option}> {selected.includes(option) ? "Remove " : "Add "} {option} </button> ))} <p>Order a {selected.join(", ")} pizza</p> </div> ); } 만일 state의 값이 Array 타입인 경우, state를 update할 때 이전 state의 Array를 그대로 변경하지말고 새로운 Array로 변경 내역을 copy해서 state에 할당해야 함을 유의합니다. 위에서도 return prev.filter(t => t !== clickedTopping); 혹은 return [clickedTopping, ...prev];으로 새로운 Array를 만들어 리턴합니다. export default function Login() { const [formState, setFormState] = useState({}); const handleChange = ({ target }) => { const { name, value } = target; setFormState((prev) => ({ ...prev, [name]: value })); }; return ( <form> <input value={formState.firstName} onChange={handleChange} name="firstName" type="text" /> <input value={formState.password} onChange={handleChange} type="password" name="password" /> </form> ); } State의 타입이 Object인 경우에도 update할 state 값은 변경된 내역을 새로 copy한 Object가 되어야 합니다. 또 Object를 arrow function에서 return할 때는 {}가 겹치는 문제가 발생할 수 있기 때문에, 반환할 Object를 ()로 감싸줄 필요가 있습니다. Separate Hooks for Separate States function Subject() { const [state, setState] = useState({ currentGrade: 'B', classmates: ['Hasan', 'Sam', 'Emma'], classDetails: {topic: 'Math', teacher: 'Ms. Barry', room: 201}; exams: [{unit: 1, score: 91}, {unit: 2, score: 88}]); }); State와 같은 dynamic data를 다루기 위해서는 state 변수마다 각각 hook을 지정해 관리하는 것이 편합니다. 위와 같이 하나의 복잡한 Object를 state로 하여 하나의 hook으로 관리한다면, 복잡한 state들을 각각 copy할 때 매우 불편해집니다. function Subject() { const [currentGrade, setGrade] = useState('B'); const [classmates, setClassmates] = useState(['Hasan', 'Sam', 'Emma']); const [classDetails, setClassDetails] = useState({topic: 'Math', teacher: 'Ms. Barry', room: 201}); const [exams, setExams] = useState([{unit: 1, score: 91}, {unit: 2, score: 88}]); // ... } 따라서, 위와 같이 state 변수마다 hook을 만들어 관리한다면 훨씬 간단하고 쉽게 state를 관리할 수 있습니다. Effect hook Effect hook은 렌더링 이후의 side effects를 관리하는 함수입니다. fetch API를 통해 백엔드로부터 데이터를 받아오거나 DOM을 읽고 변화를 주는 등의 side effect를 발생시키는 작업들을 관리하며, 보통 다음 3가지 상황에서 사용합니다. Component가 DOM에 mount되어 렌더링될 때 State 혹은 props가 변화하여 component가 다시 렌더링 될 때 Component가 DOM에서 unmount되어 렌더링될 때 Effect hook - useEffect import React, { useState, useEffect } from 'react'; function PageTitle() { const [name, setName] = useState(''); useEffect(() => { document.title = `Hi, ${name}`; }); return ( <div> <p>Use the input field below to rename this page!</p> <input onChange={({target}) => setName(target.value)} value={name} type='text' /> </div> ); } useEffect는 component를 렌더링할 때마다 다른 함수를 호출하기 위해 사용합니다. 이로 인해, useEffect는 첫 번째 인자로 렌더링 후 호출할 목적의 콜백 함수를 받습니다. 그리고 이러한 콜백 함수를 effect라고도 부릅니다. 예를 들어, 위 코드에서는 () => { document.title = name; }가 effect입니다. Effect는 현재 state에도 접근할 수 있습니다. 다만 component 렌더링이 일어난 다음 DOM이 update되면 그 후 effect가 호출되므로, state도 update가 완료된 상태에서 접근하게 됩니다. Clean Up Effects 어떠한 effect들은 메모리 누수를 피하기 위하여 항상 제거하는 작업을 동반해주어야 합니다. 예를 들어, effect를 사용해 직접 DOM 내의 element에 event listener를 추가하는 경우, 원하는 작업이 끝나면 해당 event listener를 반드시 다시 제거해주어야 합니다. 그렇지 않으면 렌더링될 때마다 호출되는 effect hook의 특성으로 인해, 이후 발생하는 수많은 렌더링 상황마다 event listener가 의도치 않게 끊임없이 추가되어 메모리가 터지는 상황이 생길 수 있습니다. 따라서 다음과 같이 useEffect의 effect 내에서 event listener를 제거하는 함수를 반환하여, 추가했던 event listener를 제거해줍니다. useEffect(()=>{ document.addEventListener('keydown', handleKeyPress); return () => { document.removeEventListener('keydown', handleKeyPress); }; }) Effect가 반환하는 함수는 useEffect가 항상 clean up 함수로 간주하므로, 리액트는 effect 작업이 끝나면 자동적으로 이를 호출합니다. Dependency array Effect는 기본적으로 매 렌더링이 일어나는 상황마다 호출됩니다. 그러나 dependency array를 사용하면, effect를 원하는 때에만 호출하도록 설정할 수 있습니다. Dependency array는 useEffect의 두 번째 인자로 넣는 array를 말합니다. 만일 component가 mount되어 첫 번째 렌더링을 할 때만 effect hook을 호출하고 최종 렌더링에서 clean up하고 싶다면, 빈 array []를 useEffect()의 두 번째 인자로 넣어줍니다. 반면에, dependency array에 특정 변수를 요소로 넣는다면, 해당 변수의 값이 변할 때만 effect가 호출됩니다. useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // Only re-run the effect if the value stored by count changes Hook을 사용하는 규칙 더욱 복잡한 React 앱에서 혼란을 피하기 위해, hook은 다음과 같은 규칙을 지키며 사용합시다. Hook을 항상 top level에서만 사용합시다. 리액트는 function component 내에서 정의한 순서에 따라 hook과 함께 관리되는 data와 function들을 인식합니다. 따라서, conditions, loops, nested functions 안에서 hook을 사용하지 말아야 합니다. if (userName !== '') { useEffect(() => { localStorage.setItem('savedUserName', userName); }); } 조건문을 쓰고 싶다면 위와 같이 쓰지 말고, 다음과 같이 effect 내에서 사용해 동일한 결과를 얻을 수 있습니다. useEffect(() => { if (userName !== '') { localStorage.setItem('savedUserName', userName); } }); Hook은 react function component 내에서만 사용합시다. Function component이외에 hook을 사용할 수 있는 곳은 custom hook을 제외하고 존재하지 않습니다. Class component나 일반적인 JavaScript 함수 내에서 hook을 사용하지 맙시다. Separate Hooks for Separate States // Handle menuItems with one useEffect hook. const [menuItems, setMenuItems] = useState(null); useEffect(() => { get('/menu').then((response) => setMenuItems(response.data)); }, []); // Handle position with a separate useEffect hook. const [position, setPosition] = useState({ x: 0, y: 0 }); useEffect(() => { const handleMove = (event) => setPosition({ x: event.clientX, y: event.clientY }); window.addEventListener('mousemove', handleMove); return () => window.removeEventListener('mousemove', handleMove); }, []); Effect hook 역시 모든 로직을 한 곳에 모아두면 가독성이 떨어지고 복잡해집니다. 따라서 위와 같이 effect 마다 따로 hook을 만드는 것을 지향합니다. Reference Learn React - Codecademy
JavaScript-Ecosystem
· 2021-08-29
React - Component Lifecycle Methods
Component lifecycle methods 리액트의 수많은 component들은 각각 자신의 lifecycle을 가집니다. 보통 component의 lifecycle 다음과 같이 구성됩니다. Mounting, when the component is being initialized and put into the DOM for the first time Updating, when the component updates as a result of changed state or changed props Unmounting, when the component is being removed from the DOM 그리고 이러한 lifecycle 각각을 제어하기 위해 개발자들이 사용할 수 있는 lifecycle method들이 존재합니다. 대표적으로 constructor()와 render() 역시 lifecycle method에 해당됩니다! constructor()는 mounting phase에 첫 번째로 호출되는 메서드로, render()는 mounting과 updating phase에 자주 등장하는 메서드로 분류할 수 있습니다. componentDidMount() componentDidMount() 메서드는 mounting phase에서 마지막으로 호출되는 메서드입니다. Mounting phase 안에서 메서드들은 다음과 같은 순서로 호출됩니다. The constructor render() componentDidMount() componentDidMount()를 활용하면 1초씩 현 시각을 계속 알려주는 시계를 만들 수 있습니다. import React from 'react'; import ReactDOM from 'react-dom'; class Clock extends React.Component { constructor(props) { super(props); this.state = { date: new Date() }; } render() { return <div>{this.state.date.toLocaleTimeString()}</div>; } componentDidMount() { // Paste your code here. const oneSecond = 1000; setInterval(() => { this.setState({ date: new Date() }); }, oneSecond); } } ReactDOM.render(<Clock />, document.getElementById('app')); componentWillUnmount import React from 'react'; export class Clock extends React.Component { constructor(props) { super(props); this.state = { date: new Date() }; } render() { return <div>{this.state.date.toLocaleTimeString()}</div>; } componentDidMount() { const oneSecond = 1000; this.intervalID = setInterval(() => { this.setState({ date: new Date() }); }, oneSecond); } componentWillUnmount() { clearInterval(this.intervalID); } } componentWillUnmount 메서드는 unmounting phase에서 사용됩니다. Component가 완전히 없어지기 전에 호출되기 때문에, side-effect를 발생시키는 불필요한 비동기 함수를 종료하기 적합한 시기입니다. 위와 같이 시간을 지속적으로 업데이트하는 시계의 setInterval() 함수를 멈추려면, componentWillUnmount() 메서드에서 clearInterval()을 사용합니다. intervalID를 clearInterval()의 인자로 전달해주면 해당 setInterval() 함수를 종료시킵니다. componentDidUpdate Updating phase에서 주로 사용하는 메서드는 render(), componentDidUpdate입니다. Update는 props와 state의 변화가 일어날 때 발생하는 작업으로, update 관련한 로직은 componentDidUpdate에서 사용하는 것이 유용합니다. Reference Learn React - Codecademy this interactive diagram
JavaScript-Ecosystem
· 2021-08-28
React - Component Interacting
Component interacting React application은 몇 십에서 몇 백 개까지 component를 가질 수 있습니다. 각각의 작은 component들은 자신의 역할을 담당하면서 거대한 app을 구성하고 서로 상호작용함으로써 app을 동작시킵니다. Component 간 상호 작용 유형 Component가 다른 component를 렌더링하는 것 Component가 다른 component에게 정보를 전달하는 것 A component in a render function class WelshCorgi extends React.Component { render() { return <h1>Welsh Corgi wooooow!</h1>; } } class Dog extends React.Component { render() { return <WelshCorgi />; } } Component 클래스의 render() 메서드는 HTML-like JSX expression 뿐만 아니라 component instance 형태의 JSX expression도 리턴할 수 있습니다. import React from 'react'; import ReactDOM from 'react-dom'; class WelshCorgi extends React.Component { render() { return <h1>Welsh Corgi wooooow!</h1>; } } class ProfilePage extends React.Component { render() { return ( <div> <h1>All About Me!</h1> <p>I like Welsh Corgi!!!</p> <WelshCorgi /> </div> ); } } 이러한 속성을 활용하면 하나의 component 안에서 다른 여러 개의 component를 함께 렌더링할 수 있습니다. 위의 <WelshCorgi /> component 인스턴스는 ProfilePage component가 생성되면 그 안에서 렌더링됩니다. 따라서, ReactDOM.render()를 통해 ProfilePage 인스턴스 하나만 렌더링하면 내부에 있는 component들은 자동으로 함께 렌더링되게 됩니다. Component 안에 다른 component가 포함되어 렌더링될 수 있다는 특징은 리액트의 강력한 장점입니다! Props 부모 component가 자식 component에가 전달하는 정보가 담긴 객체를 props라고 합니다. 모든 component들은 자신의 props를 가지고 있으며, 이를 통해 부모 component로부터 전달받은 정보를 확인할 수 있습니다. 특정 component의 props를 보고 싶다면, this.props를 사용해 확인할 수 있습니다. <Greeting name="Frarthur" town="Flundon" age={2} haunted={false} myInfo={["top", "secret", "lol"]} /> Component에 prop을 추가하고 싶다면, 생성한 인스턴스에 속성으로 추가해주면 됩니다. 위의 코드는 <Greeting /> component 인스턴스에 name, town, age, haunted, myInfo 등의 props를 부여한 것입니다. 이 때 만일 string이 아닌 정보를 주고 싶다면, {}로 정보를 감싸서 속성을 부여해야 한다는 점을 유의합시다. 이렇게 추가한 속성들은 this.props.속성이름을 통해 접근할 수 있습니다. Event handler as prop 함수 역시 props로 넘길 수 있는데, 보통 event handler 함수가 이러한 특징을 활용해 prop으로 자주 전달됩니다. import React from 'react'; import ReactDOM from 'react-dom'; class Button extends React.Component { render() { return ( <button onClick={this.props.talk}> Click me! </button> ); } } class Talker extends React.Component { talk() { let speech = ''; for (let i = 0; i < 10000; i++) { speech += 'blah '; } alert(speech); } render() { return <Button talk={this.talk} />; } } ReactDOM.render( <Talker />, document.getElementById('app') ); Event handler는 render() 메서드와 비슷한 방식으로 임의의 이름의 메서드를 정의하고 필요한 component에 prop으로서 전달합니다. Naming convention of event handler Event handler를 prop으로 전달할 때, 임의로 naming해야 할 부분이 두 군데 생깁니다. 이 때, 반드시 따를 필요는 없지만 통용되는 naming convention이 존재합니다. 첫 번째는 event handler 메서드를 정의할 때인데, 이벤트의 타입에 따라 handleClick, handleHover 등으로 사용합니다. 두 번째는 prop name인데, 이벤트 타입에 따라 onClick, onHover 등으로 정의합니다. class MyClass extends React.Component { handleHover() { alert('I am an event handler.'); alert('I will listen for a "hover" event.'); } render() { return <Child onHover={this.handleHover} />; } } this.props.children 모든 component들은 props 객체 내에 children property를 가집니다. 앞서 self-closing tag로 만들었던 component들은 사실 <MyComponentClass></MyComponentClass>로 나뉘어 쓰이는 것 역시 가능합니다. 이 경우, this.props.children은 나뉘어 쓰이는 태그 사이에 존재하는 모든 것을 리턴합니다. import React from 'react'; import ReactDOM from 'react-dom'; class List extends React.Component { render() { let titleText = `Favorite ${this.props.type}`; if (this.props.children instanceof Array) { titleText += 's'; } return ( <div> <h1>{titleText}</h1> <ul>{this.props.children}</ul> </div> ); } } class App extends React.Component { render() { return ( <div> <List type='Dog'> <li>Welsh Corgi</li> <li>Dachshund</li> </List> <List type='Cat'> <li>Road cat</li> </List> </div> ); } } ReactDOM.render( <App />, document.getElementById('app') ); 위 코드의 경우, this.props.children은 각각의 List component 사이에 있는 모든 <li> element들을 리턴합니다. <List type='Dog'>의 경우 // return elements <li>Welsh Corgi</li> <li>Dachshund</li> <List type='Cat'>의 경우 // return element <li>Road cat</li> defaultProps class Example extends React.Component { render() { return <h1>{this.props.text}</h1>; } } Example.defaultProps = { text: 'Welsh Corgi' }; 만일 component 인스턴스에 요구되어지는 prop이 전달되지 않았을 때, 해당 prop은 화면에 아무것도 출력하지 않을 것입니다. 이 때, default 값을 prop에 지정해 화면에 항상 무언가가 출력될 수 있게 할 수 있습니다. 이를 위해 component 클래스의 defaultProps property를 사용합니다. defaultProps property에 원하는 props의 기본값들을 설정한 object를 지정하여 기본값 설정을 완료합니다. state 축구 경기에서 각 팀의 스코어 정보처럼 변할 수 있는 정보를 dynamic information이라고 합니다. 리액트 component는 이러한 dynamic information을 다뤄야 할 때, props와 state를 사용합니다. 그 중, state란 각각의 component가 가지고 있는 상태를 저장한 것을 뜻하며, component 내부에서 관리됩니다. 렌더링 결과물에 영향을 주는 정보를 갖고 있다는 부분에서도 props와 공통점이 있습니다. class Example extends React.Component { constructor(props) { super(props); this.state = { mood: 'decent' }; } render() { return <div></div>; } } <Example /> 초기의 state는 props와 달리 component 클래스의 constructor에서 state property를 지정해 설정합니다. 또한, 각각의 component들은 스스로의 state를 가집니다. 모든 component는 super()를 통해 항상 초기화 시켜야 합니다. 이후, state에 적절한 객체를 할당해 initial state를 설정합니다. class TodayImFeeling extends React.Component { constructor(props) { super(props); this.state = { mood: 'decent' }; } render() { return ( <h1> I'm feeling {this.state.mood}! </h1> ); } } Component 클래스 내에서 state에 접근하고 싶다면 this.state.name-of-property 형태로 접근합니다. 위의 this.state.mood는 ‘decent’ 값에 접근합니다. this.setState() Component의 현재 state를 바꾸고 싶다면, this.setState() 메서드를 사용합니다. setState()는 변경 요소가 담긴 객체를 첫 번째 인자로 받아 사용합니다. { mood: 'great', hungry: false } 현재 state의 상황이 위와 같다고 가정해봅시다. this.setState({ hungry: true }); 그리고 setState()를 사용해 hungry 상태를 변경합니다. { mood: 'great', hungry: true } 그 결과 위와 같이 hungry 상태만 true로 변경되었습니다. setState는 기본적으로 인자로 받은 객체에 담긴 요소들만 접근해 값을 변경하고 다른 원래의 요소들은 그대로 둡니다. setState()와 render() setState() 메서드에서 유의할 점은 이 메서드가 state를 변경한 후 자동적으로 .render() 메서드까지 호출한다는 부분입니다. 즉, setState()를 사용하면 state를 변경한 부분이 바로 화면에 반영됩니다. 따라서, setState()는 render() 메서드 안에서 호출되면 안됩니다. 이를 지키지 않으면 서로 끊임없이 호출하는 무한 루프에 빠지게 됩니다. Reference Learn React - Codecademy
JavaScript-Ecosystem
· 2021-08-27
React - Component
Component of React Component란 하나의 작업을 수행하는 재사용할 수 있는 작은 코드 뭉치를 의미합니다. 여기서 하나의 작업이란 대체로 HTML 코드를 렌더링하는 것을 말합니다. Necessary import Component를 사용하기 위해서는 React 객체를 import 해두어야 합니다. React 객체에는 리액트 라이브러리를 사용하기 위한 필수적인 메서드들이 담겨있습니다. JSX expression을 사용하는데도 React 객체가 반드시 필요하므로, 첫 줄은 항상 다음 코드로 시작하도록 합니다! import React from 'react'; 또한, component 사용을 위해 ReactDOM 객체도 import합니다. ReactDOM 객체는 React 객체와 마찬가지로 React와 관련된 메서드들을 가지고 있습니다. 그러나 React에는 순수하게 React만을 위한 메서드가 담겨있는 반면, ReactDOM은 React와 DOM의 상호작용을 돕는 메서드들이 담겨 있다는 차이점이 있습니다. 따라서, 다음 코드 역시 함께 사용합니다. import ReactDOM from 'react-dom'; 클래스를 활용한 Component 생성 리액트 component는 자바스크립트의 클래스 혹은 함수를 통해 생성할 수 있습니다. 여기서는 클래스 component에 초점을 맞추겠습니다. 클래스 component는 리액트 라이브러리의 Component 클래스를 상속받아서 정의합니다. 클래스를 사용하면 원하는 만큼 인스턴스로 component를 만들어 렌더링할 수 있다는 이점이 생깁니다. import React from 'react'; import ReactDOM from 'react-dom'; class MyComponentClass extends React.Component { render() { return <h1>Hello component</h1>; } } ReactDOM.render( <MyComponentClass />, document.getElementById('app') ); 위와 같이 React.Component를 상속받으면 새로운 component 클래스를 만들어 customizing할 수 있습니다. 여기서 React.Component는 React 객체의 property이며, Component는 클래스입니다. 여기서 또 하나 유의할 점은 새로 정의한 component 클래스 body에는 반드시 render() 메서드를, render() 메서드 내에는 주로 JSX expression을 반환하는 return statement를 정의해야 한다는 부분입니다. 해당 클래스에는 어떤 component를 만들 것인지 instruction을 제시해줘야 하기 때문에, 이를 위한 render() 메서드와 return statement를 필수적으로 정의합니다. 그리고 이렇게 만들어진 클래스를 활용해 component를 자유롭게 생성할 수 있습니다. 앞서 JSX element를 사용했듯이, 클래스의 이름을 사용해 <MyComponentClass /> 코드를 쓰면 component 인스턴스가 생성됩니다! 이렇게 생성한 component 인스턴스를 ReactDOM.render()에 인자로 던져주면, 해당 component를 화면에 렌더링할 수 있습니다. Component는 클래스에서 정의한 render() 메서드를 가지고 있기 때문에, ReactDOM.render()는 인자로 받은 component의 render() 메서드를 자동으로 호출하게끔 하여 JSX expression을 반환받고 화면에 렌더링합니다. Class component의 naming convention 새로 정의한 클래스 component의 이름은 첫 글자부터 대문자를 사용하는 UpperCamelCase를 따릅니다. 이것은 Java의 naming convention에서 차용되었으며, 원래의 JavaScript 클래스를 만들 때도 마찬가지의 convention을 따릅니다. UpperCamelCase를 사용하는 또 다른 이유는 리액트 자체적으로도 찾을 수 있습니다. JSX element는 HTML-like인 경우와 component인 경우로 나뉩니다. 이 때, UpperCamelCase로 쓰인 JSX element가 있다면, 해당 element가 component instance임을 쉽게 파악할 수 있습니다. ex) ShinyBrownHairOfWelshCorgi ex) <WelshCorgiLegComponent /> render() 메서드에 정의할 수 있는 것 class Random extends React.Component { render() { // First, some logic that must happen // before rendering: const n = Math.floor(Math.random() * 10 + 1); // Next, a return statement // using that logic: return <h1>The number of Welsh Corgi is {n}!</h1>; } } Component의 render() 메서드에는 항상 return statement가 와야 합니다. 다만 이에 더하여, 렌더링 직전의 간단한 계산 역시 둘 수 있는 위치입니다. class Random extends React.Component { // This should be in the render function: const n = Math.floor(Math.random() * 10 + 1); render() { return <h1>The number of Welsh Corgi is {n}!</h1>; } }; 그러나 위와 같이 render() 메서드 바깥에 변수를 정의하는 것은 syntax error를 유발하니, 메서드 안쪽에서 정의할 것을 유의해야 합니다. Event listener in a component class MyClass extends React.Component { myFunc() { alert('Stop it. Stop hovering my Welsh Corgi.'); } render() { return ( <div onHover={this.myFunc}> </div> ); } } 위와 같이 component 클래스의 메서드로 정의한 event handler 함수를 사용하여, event listener를 component에 정의할 수 있습니다. Event listener 속성에 this를 사용해 메서드를 부여하는 것으로 적용 가능합니다. Reference Learn React - Codecademy
JavaScript-Ecosystem
· 2021-08-26
React - JSX
React basic React.js는 Facebook 엔지니어들이 개발한 UI 개발 목적의 JavaScript 라이브러리입니다. 리액트의 컴포넌트 기반 개발은 Single Page Application을 비롯한 프론트 개발에 큰 변화를 이끌었으며, 근 5~6년간 자바스크립트 생태계의 가장 중요한 존재 중 하나로 자리해 왔습니다. 최근에는 더 효율적인 프론트 개발 라이브러리들이 많이 등장했지만, 리액트의 영향력은 여전히 직간접적으로 느껴집니다. JSX const h1 = <h1>Welsh Corgi!!</h1>; JSX는 리액트에 사용되기 위해 쓰여진 JavaScript의 syntax extension입니다. 보통 JavaScript 파일 속에 JavaScript 코드와 HTML 코드들이 혼용되어 쓰여진 것들로 통용되므로, JSX 코드에는 HTML같은 코드가 포함되지만 실제로 HTML은 아닙니다. 특히, JSX는 웹 브라우저가 바로 읽을 수 없습니다. 그러므로 JSX가 포함된 JavaScript 파일을 통상적으로 사용하려면, JSX compiler를 통해 일반적인 JavaScript 코드로 컴파일해야 합니다. JSX element <h1>Hello world</h1> 또한, JavaScript 파일 속에 HTML과 똑같이 생긴 위와 같은 코드들을 JSX element라고 부릅니다. JSX element는 JavaScript 코드로 간주되어, 변수에 저장되거나 함수의 인자로 입력되는 등 일반적인 모든 프로그래밍에 문제 없이 사용됩니다. const welshCorgi = <img src='images/welsh.jpg' alt='welsh corgi' width='600px' height='600px' />; JSX element에는 HTML 때와 마찬가지로 attribute 역시 적용할 수 있습니다. const welshCorgi = ( <a href="https://www.shinybrownhair.com"> <h1> Bow wow! </h1> </a> ); Nested한 형태도 기존 HTML처럼 사용할 수 있습니다. 다만, multi-line이 될 경우 ()로 감싸주어야 오류 없이 프로그래밍할 수 있음을 유의합시다. const dogs = ( <p>I am a Poodle.</p> <p>I am a Welsh Corgi. Nice to meet you!</p> ); 다만, JSX expression은 하나의 같은 element 단위가 되어야 하기 때문에, 위와 같이 두 개의 element를 한 번에 사용하는 것은 불가능합니다. 만일 위와 같이 쓰고 싶다면, 위 코드를 하나의 <div></div> 태그로 감싸서 코드가 올바르게 동작하도록 만드는 방법을 권장합니다. Rendering 렌더링(Rendering)이란 코드를 해석해서 화면에 띄우는 작업을 의미합니다. 렌더링은 보통 리액트와 관련된 메서드들을 모아둔 ReactDom 라이브러리의 ReactDOM.render() 메서드를 사용해 진행합니다. import React from 'react'; import ReactDOM from 'react-dom'; ReactDOM.render(<h1>Hello world</h1>, document.getElementById('app')); ReactDOM.render() 메서드에는 첫 번째 인자로 화면에 띄울 JSX expression을 사용합니다. 그리고 두 번째 인자로 해당 JSX expression을 띄울 container가 될 HTML 태그를 찾아 넘깁니다. <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <link rel="stylesheet" href="/styles.css"> <title>Learn ReactJS</title> </head> <body> <main id="app"></main> </body> </html> 예를 들어 위와 같은 index.html 문서가 있다면, <main id="app"></main> 태그 속에 첫 번째 인자로 넘긴 JSX expression이 위치해 화면에 렌더링됩니다. Virtual DOM const dog = <h1>Welsh Corgi</h1>; // This will add "Welsh Corgi" to the screen: ReactDOM.render(dog, document.getElementById('app')); // This won't do anything at all: ReactDOM.render(dog, document.getElementById('app')); ReactDOM.render()의 장점은 변경이 있는 DOM elements만 update한다는 점입니다. 수많은 DOM elements가 있을 때, 변경된 것들만 update하는 것은 React의 큰 이점입니다. React는 virtual DOM을 통해 이를 실현합니다. Virtual DOM이란 리액트에서 실제 DOM object와 대응되는 가벼운 카피 버전의 가상 DOM object를 말합니다. Virtual DOM은 실제 DOM과 같은 property들을 가지지만, DOM의 변화를 화면에 직접 띄우는 기능은 없기 때문에, 일반 DOM 조작보다 빠르다는 장점이 있습니다. 따라서, 리액트는 다음과 같은 방식으로 DOM을 update합니다. 전체 virtual DOM을 업데이트합니다. Update한 virtual DOM과 이전 virtual DOM의 snapshot을 비교하여 변화된 부분들을 확인합니다. 변화된 부분만 실제 DOM object에서 update합니다. 실제 DOM의 변화가 화면에 반영됩니다. DOM manipulation의 단점 과거 일반적인 자바스크립트 라이브러리들은 DOM manipulation을 할 때, DOM element 하나가 변경되면 모든 element들을 다시 update해야 해서 비효율적이었습니다. 덕분에 DOM이 커질수록 cost가 더욱 늘어났는데, 리액트의 virtual DOM 도입은 cost 문제를 혁신적으로 해결했습니다. 변경된 특정 DOM element만 update하는 virtual DOM의 특징이 DOM manipulation 속도를 혁신적으로 향상 됐습니다. Advanced syntax of JSX JSX의 문법은 대게 HTML과 동일하지만 미묘하게 다른 부분들이 존재하므로 유의해야 합니다. className <h1 className="dog">Welsh Corgi</h1> HTML에서 사용되는 class 속성은 JSX에서 className으로 사용합니다. 이는 JavaScript가 class를 예약어로 갖고 있어서 JSX를 JavaScript로 변압할 때 키워드가 겹치는 문제가 발생하기 때문입니다. 대신 className은 JSX가 렌더링될 때, class 속성으로서 자동으로 인식됩니다. self-closing tag Fine in HTML with a slash: <br /> Also fine, without the slash: <br> HTML에서는 <img> 태그나 <input> 태그 같은 요소들의 끝 부분 > 앞에 /를 쓰는 것이 선택적입니다. Fine in JSX: <br /> NOT FINE AT ALL in JSX: <br> 하지만, JSX에서는 self-closing tag에 /를 반드시 써줘야 합니다. (그렇지 않으면, 에러가 발생합니다.) JavaScript in JSX in JavaScript import React from 'react'; import ReactDOM from 'react-dom'; ReactDOM.render( <h1>{2 + 3}</h1>, document.getElementById('app') ); // Output on monitor: 5 JSX expression 안에 일반적인 JavaScript 코드를 사용하고 싶다면, {}를 사용합니다. {} 안에 위치한 코드들은 JSX expression 안쪽이라도 JavaScript 코드로 인식됩니다. 여기서 {}는 JSX나 JavaScript가 아니라, JavaScript injection into JSX의 시작과 끝을 나타내는 marker입니다. Event Listener function myFunc() { alert('Welsh Corgi!!!!'); } <img onClick={myFunc} /> JSX에서도 HTML과 같이 event listener를 사용할 수 있습니다. on을 접두어로 하는 속성들을 사용하면 event listener를 적용할 수 있는데, 해당 속성들의 값은 반드시 함수가 되어야 합니다. 또한, HTML에서 event listener의 이름들은 모두 소문자로 쓰이지만, JSX에서는 camelCase로 사용해야 합니다. Conditional statement JSX에는 if 구문을 삽입할 수 없습니다. 하지만, 이를 해결할 몇 가지 방법도 존재합니다. const sound = 'Bow wow!'; if (sound === 'Bow wow!') { message = ( <h1> Hey, good dog! </h1> ); } else { message = ( <h1> I like a lot of animal! </h1> ); } 먼저, JSX 바깥에서 if를 사용해 원하는 조건문을 만들 수 있습니다. const sound = 'Bow wow!'; const message = ( <h1> { sound === 'Bow wow!' ? 'Hey, good dog!' : 'I like a lot of animal!' } </h1> ); 혹은 삼항연산자(ternary operator)를 사용하면 JSX 내부에서도 조건문을 사용할 수 있습니다. React에서는 상당히 자주 사용되는 방법입니다. const tasty = ( <ul> <li>Dog feed</li> { !puppy && <li>Dog gum</li> } { age > 1 && <li>bone</li> } { age > 5 && <li>Dog ade</li> } { age > 7 && <li>Dog cookie</li> } </ul> ); 만일 어떤 조건에서만 action을 취하고 다른 때는 아무 것도 하지 않는 경우라면, && 연산자를 활용하는 것도 적합합니다. 즉, && 연산자의 왼쪽 expression이 true일 경우에만, && 연산자의 오른쪽 expression이 렌더링될 것입니다. 이러한 형태의 조건문도 React에서 자주 쓰이는 방식입니다. map() const dogs = ['Welsh Corgi', 'Poodle', 'Dachshund']; const listDogs = dogs.map(dog => <li>{dog}</li>); <ul>{listDogs}</ul> 만일 JSX element의 array를 만들고 싶다면, .map()을 사용하는 것이 유용합니다. React에서 자주 사용되는 방식이므로 기억해두면 좋습니다. // This is fine in JSX, not in an explicit array: <ul> <li>dog 1</li> <li>dog 2</li> <li>dog 3</li> </ul> // This is also fine! const liArray = [ <li>dog 1</li>, <li>dog 2</li>, <li>dog 3</li> ]; <ul>{liArray}</ul> 또한, <li> JSX element들이 담긴 array는 위의 {liArray} 같이 곧바로 <ul>과 함께 사용하는 것이 가능합니다. key 속성 <ul> <li key="li-01">Dog 1</li> <li key="li-02">Dog 2</li> <li key="li-03">Dog 3</li> </ul> <li> 태그들은 때때로 key 속성을 필요로 할 때가 있습니다. 특정 상황에서 key를 설정해두지 않으면 잘못된 순서로 list-item들이 나타날 수 있으므로, 다음과 같은 상황에서는 key 속성을 설정합니다. 각각의 list-item이 memory를 가질 경우 (to-do list와 같이 항목의 체크 여부를 기억해야 할 때) list-item이 섞일 가능성이 있을 때 key 속성을 설정할 때, key 속성의 값은 unique해야 합니다. React.createElement() React 코드를 JSX expression을 쓰지 않고도 사용할 수 있는 방법이 있습니다. const h1 = <h1>Welsh Corgi</h1>; 위의 JSX expression으로 표현하던 기존의 코드는 다음과 같이 새로 쓰일 수 있습니다. const h1 = React.createElement( "h1", null, "Welsh Corgi" ); React.createElement()을 사용하면 JSX expression을 쓰지 않고도 같은 기능을 하는 React 코드를 만들 수 있습니다. 사실 JSX element가 컴파일 될 때, 컴파일러는 내부적으로 해당 JSX element를 React.createElement() 메서드로 변형하여 호출합니다. 즉, JSX expression을 사용하기 전에는 항상 import React from 'react';로 React 객체를 import해야 하는데, 그 이유는 내부적으로 항상 React.createElement() 메서드가 사용 가능해야 하기 때문입니다. Reference Learn React - Codecademy Event Listener List - React.js
JavaScript-Ecosystem
· 2021-08-25
JavaScript - Async/Await
Async-Await async, await을 사용하는 구문은 ES8에서 소개된 JavaScript의 비동기 처리를 위한 syntactic sugar입니다. 비동기 처리하는 과정이나 결과는 이전 callback 함수를 통해 구현하는 방식이나 혹은 ES6에서부터 사용하는 promise 객체를 사용해 구현하는 방식과 동일하지만, 문법적으로 조금 더 편리하게 비동기 처리를 할 수 있도록 제공됩니다. async keyword async function myFunc() { // Function body here }; myFunc(); 비동기 함수를 만들기 위해 사용하는 키워드입니다. 구현한 비동기 처리 로직은 위와 같이 async로 선언된 함수로 감싸서 의도대로 실행할 수 있습니다. const myFunc = async () => { // Function body here }; myFunc(); 또한, async 함수는 함수 선언식 뿐만 아니라 함수 표현식으로도 사용할 수 있습니다. async 함수의 리턴 값 async 함수는 항상 promise 객체를 리턴합니다. 덕분에, 원래의 promise 비동기 처리 방식대로 .then(), .catch() 등을 그대로 사용할 수 있습니다. 다만, 리턴할 때 3가지 상황에 따라 다른 promise 객체를 내어줍니다. 명시적으로 리턴하는 값이 없을 때: undefined를 resolved value로 사용하는 promise 객체를 리턴합니다. 명시적으로 promise 객체가 아닌 값을 리턴할 때: 해당 리턴 값을 resolved value로 사용하는 promise 객체를 리턴합니다. 명시적으로 promise 객체를 리턴할 때: 해당 promise 객체를 그대로 리턴합니다. await keyword async 키워드 만으로는 비동기 처리를 제대로 할 수 없기 때문에, async 함수 안에서는 보통 await을 함께 사용합니다. await은 지정한 함수에서 promise 객체가 리턴 및 resolve될 때까지 async 함수 실행 자체를 멈추었다가, promise의 resolved value를 받으면 해당 값을 리턴하고 async 함수의 남은 코드를 다시 실행하는 키워드입니다. 즉, promise를 객체를 받아 해당 promise 객체를 pending 상태에서 resolved 상태까지 실행하여 resolved value를 리턴하는 전 과정을 포괄합니다. 이러한 특이성으로 인해, await은 주로 라이브러리에서 가져온 promise를 리턴하는 함수와 함께 사용하는 것이 일반적입니다. async function asyncFuncExample(){ let resolvedValue = await myPromise(); console.log(resolvedValue); } asyncFuncExample(); // Prints: I am resolved now! 위 코드에서 myPromise()는 "I am resolved now!"라는 string을 resolve할 promise를 리턴하는 함수입니다. 이렇게 promise의 로직을 인지하며 await을 사용하면, 비동기적인 코드가 순차적인 코드 흐름으로 읽히도록 구현할 수 있습니다. Error handling with try... catch async function usingTryCatch() { try { let resolveValue = await asyncFunction('thing that will fail'); let secondValue = await secondAsyncFunction(resolveValue); } catch (err) { // Catches any errors in the try block console.log(err); } } usingTryCatch(); 기존의 promise 객체 비동기 처리 방식에서 chain이 길어질 때, .catch를 사용해도 어떤 순서에서 error가 발생한 것인지 파악하기 어려웠습니다. 반면에, async... await에서는 try... catch를 사용해 쉽게 error handling을 진행할 수 있습니다. async 함수에서 try... catch는 동기적인 코드와 같은 방식으로 error handling을 할 수 있으면서 동시에, 동기 및 비동기 error 모두를 잡아낼 수 있기 때문에, 쉬운 디버깅을 가능하게 한다는 큰 이점이 있습니다. async function usingPromiseCatch() { let resolveValue = await asyncFunction('thing that will fail'); } let rejectedPromise = usingPromiseCatch(); rejectedPromise.catch((rejectValue) => { console.log(rejectValue); }) 물론 async 함수도 promise 객체의 .catch 메서드를 종종 사용할 때가 있습니다. 위와 같이, 복잡한 코드의 마지막 에러만 잡아내고 싶을 경우 global scope에서 사용하는 것이 하나의 예입니다. 독립적인 promise들을 다루는 방법 다수의 promise 객체들이 서로 의존하고 있을 때는 promise마다 await을 사용하여 명확한 순서로 비동기 처리를 하는 것이 효율적입니다. 반면에, promise 객체들이 서로 독립적일 때는 순서에 상관없이 모든 promise가 동시에 실행되는 것이 보다 효율적입니다. async 함수에서 앞서 이야기한 concurrent 실행을 진행하는 방법을 크게 2가지 소개하겠습니다. await in one line /* 원래의 모습 async function waiting() { const firstValue = await firstAsyncThing(); const secondValue = await secondAsyncThing(); console.log(firstValue, secondValue); } */ // concurrent 실행 async function concurrent() { const firstPromise = firstAsyncThing(); const secondPromise = secondAsyncThing(); console.log(await firstPromise, await secondPromise); } use Promise.all async function asyncPromAll() { const resultArray = await Promise.all([asyncTask1(), asyncTask2(), asyncTask3(), asyncTask4()]); for (let i = 0; i<resultArray.length; i++){ console.log(resultArray[i]); } } Reference Codecademy - introduction to javascript
JavaScript-Ecosystem
· 2021-08-09
JavaScript - Browser compatibility and transpilation
Browser Compatibility & Transpilation 우리는 정기적으로 web browser의 update 알림을 받습니다. 주기적인 update가 필요한 이유는 보통 보안상 취약점을 처리하고 HTML, CSS 혹은 JavaScript의 새로운 syntax 버전을 지원하기 위해서입니다. 특히, JavaScript의 표준을 관리하는 기관, Ecma International이 2015년에 발표한 ECMAScript2015(흔히, ES6로 불리우는)가 등장했을 때, 많은 개발자들은 장점이 많은 ES6를 바로 채택하고 사용했지만 대부분의 브라우저에서 ES6가 지원되지 않아 브라우저 호환성(browser compatibility) 문제가 발생했습니다. 이 챕터에서는 새로운 syntax 버전과의 gap으로 인해 발생하는 이러한 브라우저 호환성 이슈를 개발자들이 어떤식으로 처리하는지에 초점을 맞추려고 합니다. caniuse.com caniuser.com은 브라우저 호환성 정보를 쉽게 찾아볼 수 있는 사이트입니다. 어떤 브라우저의 몇 번째 버전이 특정 라이브러리를 지원하는지 여부를 간단히 체크할 수 있습니다. 이 곳에서 검색해보면, ES5는 여전히 대다수의 브라우저에서 지원됩니다. 이와 달리, ES6는 점진적인 적용을 감안해야 합니다. 그 결과 대부분의 최신 버전 브라우저에서는 ES6가 원활히 지원되는 반면, ES6 module과 같은 ES6의 특정 feature들은 지원되지 않는 브라우저가 아직 대다수입니다. ES6의 장점과 Transpilation의 필요성 ES6는 이전 버전인 ES5에 비해 상당한 이점들이 있습니다. Readability and economy of code Addresses sources of ES5 bugs A similarity to other programming languages 이러한 장점들은 많은 web developer들이 ES6를 곧바로 채택하도록 만들었습니다. 다만, ECMA는 이로 인해 발생할 호환성 문제를 미리 예상해, ES6가 ES5 코드로 mapping될 수 있게끔 만들었습니다. 예를 들어, const나 let 키워드를 var로 mapping하거나, interpolation을 concatenation으로 mapping하는 방식입니다. 이러한 mapping은 충분히 규칙적이기 때문에, 개발자들은 ES6가 자동으로 ES5으로 변환되도록 하는 Babel이라는 JS library를 만들었습니다. 즉, 하나의 프로그래밍 언어를 다른 언어로 변환하는 과정을 transpilation이라고 하며, Babel은 ES6를 ES5로 transpile합니다. Babel Babel은 ES6를 ES5로 손쉽게 transpile해주는 library입니다. 먼저, Babel을 사용하기 위해 ES6의 파일(main.js)의 위치를 ./src/main.js에 둡니다. project |_ src |___ main.js // ES6 file 그리고 Babel을 설치하기 전에 npm을 사용할 수 있게끔 프로젝트를 setup해야 합니다. npm은 node project manager의 약자로 node package에 대한 접근과 관리를 위해 사용됩니다. npm을 사용하면 작업의 반복과 버그를 줄일 수 있습니다. 터미널에서 npm을 init합니다. (Node가 설치되어 있어야 합니다!) npm init 이 때, metadata에 관한 사항을 적어달라는 prompt가 나오는데, title과 description정도만 입력하고 전부 무시해도 좋습니다. (title, description 역시 선택사항입니다.) Init 이후에, root 디렉토리에는 package.json 파일이 생성됩니다. package.json 파일은 해당 프로젝트의 metadata와 프로젝트를 실행하기 위해 필요한 node package 목록, command line scripts에 해당하는 key-value pair 등을 저장합니다. Babel은 터미널 창에서 다음과 같이 사용합니다. Babel package 설치하기 (2개 모두) for CLI tool npm install babel-cli -D for mapping information npm install babel-preset-env -D 실행이 완료되면 Babel 패키지 및 관련된 모든 dependency들이 node_modules 디렉토리에 저장되어 있는 것을 확인할 수 있습니다. -D 옵션 -D는 해당 패키지를 package.json의 devDependencies라는 property에 추가하는 옵션입니다. 일단 devDependencies에 추가된 패키지들은 다른 개발자들이 현재 프로젝트를 실행할 때 각각의 패키지를 install할 필요없이 npm install 커맨드 한 번으로 모두 설치되는 이점을 가집니다. Source가 되는 JS version 설정하기 Root 디렉토리에서 .babelrc 파일을 생성합니다. touch .babelrc .babelrc 내에 source가 될 js 파일의 버전을 명시합니다. 아래와 같은 object를 파일에 저장하면, Babel은 ES6+에 대한 코드들을 목표 언어로 변환할 것입니다. ``` { “presets”: [“env”] } package.json에 Babel 실행을 위한 script 기재하기 package.json에 script property에 가보면 다음과 같은 객체가 존재함을 확인할 수 있습니다. ... "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, ... 이 객체의 "test" property 밑에, 다음과 같이 Babel을 실행하기 위한 script를 하나 추가합니다. ... "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "babel src -d lib" } 추가한 코드는 각각 다음과 같은 의미를 가지고 있습니다. babel — The Babel command call responsible for transpiling code. src — Instructs Babel to transpile all JavaScript code inside the src directory. -d — Instructs Babel to write the transpiled code to a directory. lib — Babel writes the transpiled code to a directory called lib. Babel 실행하기 (root directory) npm run build 작업이 완료되면 ./lib 디렉토리에 변환된 ES5 코드가 담긴 파일을 확인할 수 있습니다. 파일명은 본래의 ES6 파일명과 동일하게 생성됩니다. 최종적인 디렉토리 구조는 다음과 같습니다. project |_ lib |___ main.js |_ node_modules |___ .bin |___ ... |_ src |___ main.js |_ .babelrc |_ package.json Reference Codecademy - introduction to javascript
JavaScript-Ecosystem
· 2021-08-08
JavaScript - Class
Class Javascript는 OOP(object-oriented programming) language입니다. 따라서, 실제 세계를 모델로 class와 instance를 만들어 object들을 사용할 수 있습니다. Syntax example class Dog { constructor(name) { this._name = name; this._behavior = 0; } get name() { return this._name; } get behavior() { return this._behavior; } incrementBehavior() { this._behavior ++; } } const halley = new Dog('Halley'); console.log(halley.name); // Print name value to console console.log(halley.behavior); // Print behavior value to console halley.incrementBehavior(); // Add one to behavior console.log(halley.name); // Print name value to console console.log(halley.behavior); // Print behavior value to console class Class를 생성하기 위해 필요한 키워드 입니다. constructor Class가 object와 가장 구분되는 지점은 constructor 메서드의 유무입니다. constructor는 새로운 instance가 만들어질 때마다 호출되는 class의 중요한 메서드이며, instance를 초기화하는 역할을 합니다. this Class의 맥락에서 this는 해당 class의 instance를 의미합니다. new Class의 instance를 생성하기 위해 사용하는 키워드입니다. new는 class 내의 constructor() 메서드를 호출하고 새로운 instance를 반환합니다. 상속(Inheritance) class Cat { constructor(name, usesLitter) { this._name = name; this._usesLitter = usesLitter; this._behavior = 0; } get name() { return this._name; } get behavior() { return this._behavior; } get usesLitter() { return this._usesLitter; } incrementBehavior() { this._behavior++; } } 앞선 class의 예제에서 Dog class를 만들었던 것처럼, Cat class도 이와 유사하게 만들 수 있습니다. 여기선 Cat의 경우 모든 것이 Dog와 동일하지만 배변기 사용 가능 여부를 나타내는 usesLitter property만 하나 더 가집니다. 이렇게 여러 class가 동일한 부분을 가질 경우, 코드의 반복을 피하기 위해 부모 class를 만들어 자식 class가 이를 상속(inheritance)받게끔 설계하는 것이 효율적입니다. 상속은 부모 class가 가지는 property와 method를 동일하게 사용할 수 있게끔 물려받는 것을 의미합니다. 상속을 사용하면 코드의 가독성이 높아지고 유지보수가 매우 쉬워집니다. class Animal { constructor(name) { this._name = name; this._behavior = 0; } get name() { return this._name; } get behavior() { return this._behavior; } incrementBehavior() { this._behavior++; } } 따라서, 위와 같이 Animal class를 만들어 Dog와 Cat의 공통된 부분을 모은 후, 이를 각자 상속받도록 설계하는 것이 보다 나은 코드를 만드는 방향이 될 것입니다. Animal을 상속받은 Cat의 코드는 다음과 같습니다. class Cat extends Animal { constructor(name, usesLitter) { super(name); this._usesLitter = usesLitter; } get usesLitter() { return this._usesLitter; } } extends Class를 다른 class의 자식 class로 만들기 위해 사용하는 키워드입니다. 부모 class의 method들을 자식 class가 사용할 수 있게 됩니다. super 부모 class의 constructor 메서드를 호출하는 키워드입니다. 부모 class의 property 상속과 관련이 깊습니다. 또한, 자식 class에서 this를 사용하기 위해, 자식 class 내의 constructor 메서드 첫 번째 줄에 반드시 호출해주어야 합니다. (그렇지 않으면, reference error가 발생합니다!) const bryceCat = new Cat('Bryce', false); console.log(bryceCat._name); // output: Bryce console.log(bryceCat.name); // output: Bryce 끝으로, Animal class를 상속받은 Cat은 위와 같이 instance를 만들어 사용할 수 있습니다. Static method Static method는 class에 직접적으로 접근해 사용하는 메서드를 말합니다. 해당 class의 instance를 통해서는 사용할 수 없다는 특징이 있습니다. 예를 들어, Date class는 instance를 만들 수 있으면서 .now() 같은 static method를 사용할 수 있습니다. 다음은 Animal class에 static method generateName을 추가한 코드입니다. class Animal { constructor(name) { this._name = name; this._behavior = 0; } static generateName() { const names = ['Angel', 'Spike', 'Buffy', 'Willow', 'Tara']; const randomNumber = Math.floor(Math.random()*5); return names[randomNumber]; } } console.log(Animal.generateName()); // returns a name static Static method를 선언하는 키워드입니다. static이 사용된 메서드는 instance를 통해 사용할 수 없고, class에서 직접적으로 접근해야 호출 가능합니다. Instance를 통해 호출할 경우, error를 일으킵니다. Reference Codecademy - introduction to javascript
JavaScript-Ecosystem
· 2021-08-07
JavaScript - Object
Object Javascript의 data type은 6개의 primitive data type(string, number, boolean, null, undefined, symbol)과 1개의 object data type으로 구성되어 있습니다. Javascript는 객체지향 언어이고 6개의 primitive data type도 객체와 같이 동작하는 특징이 있습니다. 또한, object는 mutable(변경가능한) 속성을 가집니다. Syntax Object는 {}를 통해 구현됩니다. {} 안에는 unordered data를 key-value pair로 삽입합니다. value의 경우 어떤 data type이 와도 괜찮습니다. 반면에, key의 타입은 string이어야 합니다. 다만, key의 경우 특별한 특수문자를 집어넣는 것이 아니라면 quotation mark 없이 사용해도 string으로 자동 인식됩니다. // An object literal with two key-value pairs let spaceship = { 'Fuel Type': 'diesel', color: 'silver' }; Property Object에 저장된 함수가 아닌 data는 property라고 부릅니다. Property에 접근할 때는 .이 사용됩니다. 만일 object 내에 없는 property에 접근한 경우에는 undefined가 반환됩니다. let spaceship = { homePlanet: 'Earth', color: 'silver' }; spaceship.homePlanet; // Returns 'Earth', spaceship.color; // Returns 'silver', 또 다른 방법은 []을 사용하는 것입니다. 원하는 key를 []안에 넣으면 object에서 해당하는 property에 접근합니다. []는 특수문자가 포함된 key string에 특히 유용합니다. let spaceship = { 'Fuel Type': 'Turbo Fuel', 'Active Duty': true, homePlanet: 'Earth', numCrew: 5 }; spaceship['Active Duty']; // Returns true spaceship['Fuel Type']; // Returns 'Turbo Fuel' spaceship['numCrew']; // Returns 5 spaceship['!!!!!!!!!!!!!!!']; // Returns undefined Add, update and delete [], .와 =를 사용하면, object에 새로운 property를 추가하거나 기존 property를 수정할 수 있습니다. 또한, const 변수에 담긴 object여도 해당 object 안의 property를 추가하거나 수정할 수 있습니다. const spaceship = {type: 'shuttle'}; spaceship = {type: 'alien'}; // TypeError: Assignment to constant variable. spaceship.type = 'alien'; // Changes the value of the type property spaceship.speed = 'Mach 5'; // Creates a new key of 'speed' with a value of 'Mach 5' Object 내의 property를 삭제하는 방법은 delete 키워드를 사용하는 것입니다. 역시 const 변수에 담긴 object여도 내부의 property 삭제가 가능합니다. const spaceship = { 'Fuel Type': 'Turbo Fuel', homePlanet: 'Earth', mission: 'Explore the universe' }; delete spaceship.mission; // Removes the mission property Method Object 내에 저장된 데이터가 함수라면, 해당 데이터는 method라고 부릅니다. Method는 key에 method 이름을, value에 익명 함수를 저장함으로써 구현합니다. const alienShip = { invade: function () { console.log('Hello! We have come to dominate your planet. Instead of Earth, it shall be called New Xaculon.') } }; ES6에서 새로이 소개된 method 문법에서는 :과 function 키워드 없이도 정의할 수 있습니다. const alienShip = { invade () { console.log('Hello! We have come to dominate your planet. Instead of Earth, it shall be called New Xaculon.') } }; Method는 ., ()를 사용해 호출합니다. alienShip.invade(); // Prints 'Hello! We have come to dominate your planet. Instead of Earth, it shall be called New Xaculon.' Pass by reference Javascript에서 object는 pass by reference로 동작합니다. Object를 담는 변수는 실제로는 해당 객체가 담겨 있는 메모리 주소를 담기 때문에, object가 함수에 인자로 전달되어 변형이 일어나면 함수 밖의 실제 object도 영향을 받아 변형됩니다. const spaceship = { homePlanet : 'Earth', color : 'silver' }; let paintIt = obj => { obj.color = 'glorious gold' }; paintIt(spaceship); spaceship.color // Returns 'glorious gold' 함수 내에서 object를 재할당하는 경우 let spaceship = { homePlanet : 'Earth', color : 'red' }; let tryReassignment = obj => { obj = { identified : false, 'transport type' : 'flying' } console.log(obj) // Prints {'identified': false, 'transport type': 'flying'} }; tryReassignment(spaceship) // The attempt at reassignment does not work. spaceship // Still returns {homePlanet : 'Earth', color : 'red'}; spaceship = { identified : false, 'transport type': 'flying' }; // Regular reassignment still works. 함수의 인자로 object를 받을 때, 함수 내에서 새로운 object를 재할당을 하는 것은 기존 object에 영향을 미치지 않습니다. 위 예에서 obj 파라미터는 함수내에 생성되는 로컬 변수입니다. tryReassignment 함수의 흐름은 파라미터 obj에 인자로 들어온 object의 메모리 주소가 담기고, 이에 대해 새로운 object를 할당하여 새 object의 메모리 주소가 다시 obj에 담기게끔 이어집니다. 하지만, 함수가 종료되면 로컬 변수였던 obj 역시 사라지기 때문에, 기존 spaceship에 담긴 object는 변형 없이 그대로 남아 있게 됩니다. for … in Array의 경우 index를 통해 looping할 수 있지만, object는 key를 사용하기 때문에 다른 looping 수단이 필요합니다. 따라서, object looping에 대해서는 for ... in 구문을 사용합니다. let spaceship = { crew: { captain: { name: 'Lily', degree: 'Computer Engineering', cheerTeam() { console.log('You got this!') } }, 'chief officer': { name: 'Dan', degree: 'Aerospace Engineering', agree() { console.log('I agree, captain!') } }, medic: { name: 'Clementine', degree: 'Physics', announce() { console.log(`Jets on!`) } }, translator: { name: 'Shauna', degree: 'Conservation Science', powerFuel() { console.log('The tank is full!') } } } }; // for...in for (let crewMember in spaceship.crew) { console.log(`${crewMember}: ${spaceship.crew[crewMember].name}`); } this keyword this 키워드는 calling object를 나타내며, object의 method 내에서 property에 접근할 때는 this 키워드를 사용합니다. 여기서 calling object란 해당 method를 호출하는 객체를 말합니다. const goat = { dietType: 'herbivore', makeSound() { console.log('baaa'); }, diet() { console.log(this.dietType); } }; goat.diet(); // Output: herbivore 예를 들어, diet() method에서 dietType property에 접근하기 위해서는 반드시 this 키워드가 필요합니다. diet() 내에서 dietType에 접근할 경우 scope가 diet() 안쪽으로 설정되기 때문에 reference error가 발생합니다. 따라서, dietype property에 접근하려면 this 키워드로 calling object인 goat를 불러와 접근해야 합니다. Arrow function과 this const goat = { dietType: 'herbivore', makeSound() { console.log('baaa'); }, diet: () => { console.log(this.dietType); } }; goat.diet(); // Prints undefined 객체에 method를 정의할 때, arrow function 사용은 지양해야 합니다. 위와 같은 경우 this가 가리키는 calling object는 global object입니다. this가 diet scope에 존재하지 않기 때문에, 상위 스코프를 탐색하게 되고 global object가 this가 됩니다. 따라서, global object에는 dietType property가 없기 때문에, this.dietType은 undefined를 가집니다. Privacy of object Product를 만들다보면, 어떠한 object 내 property에 아무나 접근하지 못하게끔 막아야 하는 상황이 발생합니다. 특정 프로그래밍 언어들에서는 이러한 경우를 제어할 수 있는 privacy와 관련된 built-in 키워드를 제공합니다. 하지만 Javascript의 경우 이러한 제어 방법이 없기 때문에, 네이밍 컨벤션을 통해 다른 개발자들에게 해당 property를 어떻게 써야할 지 알려줍니다. 대표적으로 property의 식별자 앞에 _를 붙이는 것은 해당 property가 변형되어서는 안된다는 의미입니다. const robot = { _energyLevel: 100, recharge(){ this._energyLevel += 30; console.log(`Recharged! Energy is currently at ${this._energyLevel}%.`) } }; robot['_energyLevel'] = 'high'; robot.recharge(); // Output: Recharged! Energy is currently at high30%. 예를 들어, 위 코드의 경우 _energyLevel은 robot['_energyLevel'] = 'high';과 같이 실제로 변형이 가능합니다. 하지만, 개발자의 의도에 맞지 않게 string 값으로 변형함으로 인해 high30%와 같은 어색한 결과가 발생했습니다. 이처럼 _가 붙은 property는 원치않는 결과가 나올 수 있으니 직접적으로 접근하여 변형시키면 안된다는 의미를 내포합니다. Getters & Setters Getters method const person = { _firstName: 'John', _lastName: 'Doe', get fullName() { if (this._firstName && this._lastName){ return `${this._firstName} ${this._lastName}`; } else { return 'Missing a first name or a last name.'; } } } // To call the getter method: person.fullName; // 'John Doe' Getters는 객체 내부에서 property를 가져와 반환해주는 method입니다. Method 앞에 get를 사용해 구현하며, this를 통해 객체 내의 property를 조작합니다. Getters를 호출할 때는 마치 property에 접근하는 것 같이, () 없이 .만으로 호출합니다. Getters를 사용하면, property에 접근할 때 원하는 action을 임의로 추가할 수 있고, 다른 개발자들이 이해하기 쉽도록 코드를 짤 수 있습니다. Setters method const person = { _age: 37, set age(newAge){ if (typeof newAge === 'number'){ this._age = newAge; } else { console.log('You must assign a number to age'); } } }; person.age = 40; console.log(person._age); // Logs: 40 person.age = '40'; // Logs: You must assign a number to age 객체 내 property에 대한 접근을 도와주는 getters와 달리, setters는 객체 내 존재하는 property의 value를 재할당할 수 있게 도와주는 method입니다. Method 앞에 set을 사용해 구현하며, 마찬가지로 this를 사용해 객체 내 property를 조작합니다. Setters를 호출할 때도 마치 property에 값을 할당하는 것 같이 .만 사용하여 호출합니다. Setters도 input checking, easier readability 등의 이점을 가집니다. Naming of getters, setters Getters와 setters의 이름은 객체 내의 property들의 이름과 겹쳐서는 안됩니다. 만일 겹칠 경우, 끝없는 call stack error에 빠지게 됩니다. 이를 피하기 위해, property 이름 앞에 _를 붙여주는 것은 좋은 방법이 됩니다. Factory function const monsterFactory = (name, age, energySource, catchPhrase) => { return { name: name, age: age, energySource: energySource, scare() { console.log(catchPhrase); } } }; const ghost = monsterFactory('Ghouly', 251, 'ectoplasm', 'BOO!'); ghost.scare(); // 'BOO!' 하나하나의 object를 직접 만드는 것은 손이 많이 가고 비효율적입니다. 따라서, 몇 가지 parameter를 받아서 customized된 object를 반환하는 함수를 만들면 다수의 object를 효율적으로 생성할 수 있습니다. 이러한 함수를 factory function이라고 합니다. Property value shorthand const monsterFactory = (name, age) => { return { name: name, age: age } }; 기존에는 객체에 property를 저장하기 위해 위 코드와 같이 key-value pair 방식을 사용했습니다. 다만, ES6에서는 factory function을 사용할 때와 같이 parameter의 이름과 property의 이름이 같은 경우에 대해 코드 중복을 줄일 수 있도록 property value shorthand 문법을 제공합니다. 따라서 위 코드는 다음과 같이 수정될 수 있습니다. const monsterFactory = (name, age) => { return { name, age } }; Destructured assignment 객체의 key를 통해 value를 가져와 변수에 저장하던 일반적인 방식에 대해, 조금 더 간략한 destructured assignment 방식이 존재합니다. const vampire = { name: 'Dracula', residence: 'Transylvania', preferences: { day: 'stay inside', night: 'satisfy appetite' } }; 위와 같은 vampire 객체에서 residence property를 가져와 변수에 저장하고 싶다면, 두 가지 방식을 사용할 수 있습니다. const residence = vampire.residence; console.log(residence); // Prints 'Transylvania' 먼저 일반적인 방식으로 key를 통해 가져올 수 있습니다. const { residence } = vampire; console.log(residence); // Prints 'Transylvania' 그런데 만일 key의 이름과 같은 이름으로 변수를 생성한다면, 위와 같이 {}를 통해 보다 간결히 property를 가져와 변수에 저장할 수 있습니다. Reference Codecademy - introduction to javascript
JavaScript-Ecosystem
· 2021-08-06
JavaScript - Iterator
Iterator Looping을 더욱 쉽게 만들어주는 JavaScript의 built-in array methods를 iteration method(=iterator)라고 합니다. Iterator는 array가 element들을 조작하고 value를 반환하기 위해 호출하는 메서드로서 도움을 줍니다. forEach() forEach()는 특정 함수를 array 각각의 element들에 적용하는 iterator입니다. 보통 iterator의 인자로 함수를 받은 후, element들 각각을 인자로 사용해 해당 함수를 호출합니다. (이렇게 다른 함수의 인자로 사용되는 함수를 callback 함수라고 부릅니다.) forEach()는 기존의 array를 변경하지 않으며, undefined를 return합니다. groceries.forEach(groceryItem => console.log(groceryItem)); 또한, arrow function을 인자로 사용해 iterator를 호출할 수도 있습니다. 이처럼, iterator의 인자로 사용되는 함수의 syntax는 임의로 자유롭게 사용할 수 있습니다. map() const numbers = [1, 2, 3, 4, 5]; const bigNumbers = numbers.map(number => { return number * 10; }); console.log(numbers); // Output: [1, 2, 3, 4, 5] console.log(bigNumbers); // Output: [10, 20, 30, 40, 50] map() 역시 forEach()와 비슷하게 동작합니다. 인자로 callback 함수를 받아, array 각각의 element를 callback 함수의 인자로 사용합니다. 다만, map()은 함수를 적용한 새로운 값들을 array에 담아서 반환한다는 점이 특징입니다. filter() const words = ['chair', 'music', 'pillow', 'brick', 'pen', 'door']; const shortWords = words.filter(word => { return word.length < 6; }); console.log(words); // Output: ['chair', 'music', 'pillow', 'brick', 'pen', 'door']; console.log(shortWords); // Output: ['chair', 'music', 'brick', 'pen', 'door'] filter()는 원래의 array에서 특정 조건에 만족하는 element들만 골라내어 새로운 array에 담아 반환합니다. 따라서, filter()에 인자로 사용되는 callback 함수는 반드시 boolean 값을 리턴하는 함수여야 합니다. 이 때, callback 함수가 true를 반환하게 하는 element들이 새로운 array에 담깁니다. findIndex() const jumbledNums = [123, 25, 78, 5, 9]; const lessThanTen = jumbledNums.findIndex(num => { return num < 10; }); console.log(lessThanTen); // Output: 3 console.log(jumbledNums[3]); // Output: 5 findIndex()는 특정 element의 위치를 알고 싶을 때 사용하는 iterator입니다. Callback 함수가 true를 반환하는 첫 번째 element의 index를 return합니다. 만일, callback 함수의 조건을 충족하는 element가 없다면 findIndex()는 -1을 반환합니다. reduce() const numbers = [1, 2, 4, 10]; const summedNums = numbers.reduce((accumulator, currentValue) => { return accumulator + currentValue }) console.log(summedNums) // Output: 17 Iteration accumulator currentValue return value First 1 2 3 Second 3 4 7 Third 7 10 17 reduce()는 말그대로 array을 감소시켜 하나의 값으로 만드는 iterator입니다. Callback 함수에 따라 array의 각 element를 accumulator에 대해 계산해, 최종적으로 하나의 계산 값을 반환합니다. const numbers = [1, 2, 4, 10]; const summedNums = numbers.reduce((accumulator, currentValue) => { return accumulator + currentValue }, 100) // <- Second argument for .reduce() console.log(summedNums); // Output: 117 Iteration # accumulator currentValue return value First 100 1 101 Second 101 2 103 Third 103 4 107 Fourth 107 10 117 또한, reduce()는 optional한 두 번째 parameter를 받을 수 있으며, 이 때 해당 parameter는 accumulator로서 사용됩니다. Reference Codecademy - introduction to javascript
JavaScript-Ecosystem
· 2021-08-04
JavaScript - Array
Array Javascript의 array는 어떤 data type도 저장할 수 있으며, 저장된 data마다 순서(index)를 지닙니다. Syntax 기본적인 문법은 []을 중심으로 이뤄지며, array 내부에 다양한 type의 data들이 함께 저장될 수 있습니다. Indexing Array는 []에 index를 사용하여 원하는 element에 접근할 수 있습니다. Indexing의 시작은 0부터 진행되며, 만일 element의 총 개수를 넘어가는 index로 접근할 경우 undefined가 반환됩니다. Indexing to String const hello = 'Hello World'; console.log(hello[6]); // Output: W 또한, Indexing은 String type의 data에도 적용됨을 유의합니다. Update with index let seasons = ['Winter', 'Spring', 'Summer', 'Fall']; seasons[3] = 'Autumn'; console.log(seasons); //Output: ['Winter', 'Spring', 'Summer', 'Autumn'] Indexing을 사용하면 접근한 data를 원하는 값으로 update하는 것도 가능합니다. let & const in array let condiments = ['Ketchup', 'Mustard', 'Soy Sauce', 'Sriracha']; const utensils = ['Fork', 'Knife', 'Chopsticks', 'Spork']; condiments[0] = 'Mayo'; console.log(condiments); // [ 'Mayo', 'Mustard', 'Soy Sauce', 'Sriracha' ] condiments = ['Mayo']; console.log(condiments); // [ 'Mayo' ] utensils[3] = 'Spoon'; console.log(utensils); // [ 'Fork', 'Knife', 'Chopsticks', 'Spoon' ] const 변수에 저장한 array라도 해당 array 내부의 요소는 여전히 변경가능(mutable)합니다. 다만, 새로운 array 혹은 값을 변수에 재할당할 수는 없습니다. Useful property & method length: array 내의 존재하는 element의 개수를 반환합니다. push(): array의 맨 끝에 element를 추가합니다. (이 때, 인자를 여러 개 받을 수 있습니다.) const itemTracker = ['item 0', 'item 1', 'item 2']; itemTracker.push('item 3', 'item 4'); console.log(itemTracker); // Output: ['item 0', 'item 1', 'item 2', 'item 3', 'item 4']; pop(): array의 맨 끝의 element를 제거하고 그 값을 반환합니다. (인자를 받지 않습니다.) shift(): array 맨 앞의 element를 제거하고 반환합니다. (인자를 받지 않습니다.) unshift(): array의 맨 앞에 element를 추가합니다. (이 때, 인자를 여러 개 받을 수 있습니다.) slice(): 설정한 index대로 slicing한 결과를 반환합니다. const animals = ['ant', 'bison', 'camel', 'duck', 'elephant']; console.log(animals.slice(2)); // expected output: Array ["camel", "duck", "elephant"] console.log(animals.slice(2, 4)); // expected output: Array ["camel", "duck"] console.log(animals.slice(1, 5)); // expected output: Array ["bison", "camel", "duck", "elephant"] console.log(animals.slice(-2)); // expected output: Array ["duck", "elephant"] console.log(animals.slice(2, -1)); // expected output: Array ["camel", "duck"] indexOf(): 인자로 오는 값이 array에서 몇 번째 index인지 찾아 반환합니다. Reference Codecademy - introduction to javascript
JavaScript-Ecosystem
· 2021-07-24
JavaScript - Function
Function of Javascript Syntax Syntax of declaring parameter and calling with argument Parameter를 함수에 선언하는 문법은 다음과 같습니다. 그리고 다음과 같이 인자를 전달해 해당 함수를 호출합니다. Default parameter Javascript에서 default parameter는 ES6 버전에서 소개되었습니다. 함수에 default parameter를 설정해두면, 인자가 전달되지 않거나 data type이 undefined인 인자가 전달될 때 argument의 값으로 default parameter에 설정된 값이 오게 됩니다. function greeting (dog = 'stranger dog') { console.log(`Hello, ${dog}!`) } greeting('Welsh Corgi') // Output: Hello, Welsh Corgi! greeting() // Output: Hello, stranger dog! Return 보통의 언어들처럼 return 키워드를 사용해 함수의 결과를 반환합니다. 만일 어떤 값을 리턴하지 않으면, 기본적으로 undefined 값이 반환됩니다. function greeting(name) { let text = 'Bow wow, hello ' + name + '!'; } console.log(greeting('lucian')) // Prints undefined function greeting(name) { let text = 'Bow wow, hello ' + name + '!'; return; } console.log(greeting('lucian')) // Prints undefined Hoisting Javascript는 함수가 선언된 곳 이전에서도 해당 함수를 호출할 수 있습니다. 이러한 Javascript의 특징을 hoisting이라고 부릅니다. 다만, 함수 선언 이전에 호출하는 것은 일반적으로 좋은 방법이 아니기 때문에, hoisting의 사용은 지양하는 것이 좋습니다. greeting(); // Output: Hello, World! function greeting() { console.log('Hello, bow wow!'); } 함수 표현식 (Function Expression) 함수를 정의하는 또 다른 방법으로 함수 표현식이 있습니다. 보통의 함수 선언식과 달리 함수 표현식은 익명함수를 변수에 저장하는 방식으로 구현합니다.(ES6부터 보통 const 변수에 담습니다.) 익명함수는 식별자(이름)가 없는 함수를 말합니다. 함수 표현식의 기본 문법 예제는 다음과 같습니다. 함수 표현식으로 만든 함수는 변수의 이름을 사용해서 호출합니다. variableName(argument1, argument2) 함수 표현식에서 또 한 가지 유의할 점은 hoisting이 적용되지 않는다는 것입니다. 함수 표현식은 항상 함수를 호출하기 전에 위치해야 합니다. Arrow function 함수를 짧게 정의하도록 돕는 또 하나의 방법입니다. function 키워드를 쓰는 대신 =>를 써서 함수를 선언합니다. 다음은 arrow function의 syntax 예제입니다. const greeting = (name) => { let text = `Hi, ${name}. Bow wow!` return text; }; console.log(greeting('Lucian')); // Hi, Lucian. Bow wow! Concise arrow function Arrow function은 몇 가지 조건 하에서 더욱 간결해질 수 있습니다. 먼저, 함수의 parameter가 한 개라면, () 없이 parameter를 선언할 수 있습니다. 함수의 body가 single-line block일 경우, {}은 생략할 수 있습니다. {}이 없는 경우, 해당 body의 결과는 return 키워드에 상관없이 자동으로 반환됩니다. Reference Codecademy - introduction to javascript 함수 표현식 VS 함수 선언식
JavaScript-Ecosystem
· 2021-07-23
JavaScript - First step
First step of Javascript 출력 console 객체의 log 메서드를 사용해 콘솔에 출력합니다. console.log("print out something"); 주석 처리 Single line comment // something comment Multi-line comment /* something comment */ Fundamental data types Number: Any number, including numbers with decimals: 4, 8, 1516, 23.42. String: Any grouping of characters on your keyboard (letters, numbers, spaces, symbols, etc.) surrounded by single quotes: ' ... ' or double quotes " ... ". Though we prefer single quotes. Some people like to think of string as a fancy word for text. Boolean: This data type only has two possible values— either true or false (without quotes). It’s helpful to think of booleans as on and off switches or as the answers to a “yes” or “no” question. Null: This data type represents the intentional absence of a value, and is represented by the keyword null (without quotes). Undefined: This data type is denoted by the keyword undefined (without quotes). It also represents the absence of a value though it has a different use than null. Symbol: A newer feature to the language, symbols are unique identifiers, useful in more complex coding. No need to worry about these for now. Object: Collections of related data. Object를 제외한 나머지 6개의 data types는 Primitive data type이라고 부릅니다. Operator Javascript에는 다음과 같은 산술 연산자들이 존재합니다. Add: + (복합 대입 연산자는 +=) Minus: - (복합 대입 연산자는 -=) Multiply: * (복합 대입 연산자는 *=) Divide: / (복합 대입 연산자는 /=) Modulo: % (복합 대입 연산자는 %=) Increment operator: ++ (+1을 함과 동시에 할당까지 진행) Decrement operator: -- (-1을 함과 동시에 할당까지 진행) 비교 연산자는 ===를 제외하고는 다른 언어들과 비슷한 양상을 보입니다. 비교 대상은 Number 뿐만 아니라 String도 포함됩니다. Less than: < Greater than: > Less than or equal to: <= Greater than or equal to: >= Is equal to: === Is not equal to: !== 논리 연산자는 다음과 같이 사용합니다. And: && Or: || Not: ! 변수 (Variable) Javascript에서는 camel case가 변수명 convention으로 사용됩니다. favoriteFood, numOfSlices, etc… 또한, 변수는 값을 꼭 할당할 필요 없이 선언만 할 수도 있습니다. 이렇게 선언만 한 경우, 해당 변수에는 자동적으로 undefined 값이 정의됩니다. let price; console.log(price); // Output: undefined price = 350; console.log(price); // Output: 350 Javascript에서는 변수를 선언하는 키워드의 종류로 var, let, const가 있습니다. var: 새로운 변수를 생성할 수 있게 해주는 기본 키워드입니다. 2015년 등장한 ES6 버전 이전에 가장 많이 쓰였습니다. let: 변수에 다른 값이 재할당될 수 있음을 의미하는 키워드입니다. ES6 버전에서 처음 등장했습니다. let dog = 'Welsh Corgi'; console.log(dog); // Output: Welsh Corgi dog = 'Poodle'; console.log(dog); // Output: Poodle const: 변수에 다른 값이 재할당될 수 없음을 의마하는 키워드입니다. 실제로 다른 값을 재할당하면 TypeError가 발생합니다. 또한, 변수는 선언함과 동시에 값이 할당되어야 합니다. 선언만 할 경우 SyntaxError가 발생합니다. let과 마찬가지로 ES6 버전에서 처음 등장했습니다. String concatenation Javascript에서도 +를 사용해 string 간의 concatenation을 수행할 수 있습니다. const favoriteAnimal = 'Welsh Corgi'; console.log('My favorite animal: ' + favoriteAnimal); // My favorite animal: Welsh Corgi 만일 data type이 String이 아닌 data와 concatenation을 할 경우, String type으로 auto converting 되어 정상적으로 concatenation됩니다. const count = 3; console.log('There are ' + count + ' Welsh Corgies!'); // There are 3 Welsh Corgies! String interpolation ES6 버전에서는 template literal을 사용해 변수를 string에 삽입하는 interpolation을 수행할 수 있습니다. Interpolation은 `` 를 사용해 표현하며, placeholder ${변수명}`를 사용해 변수를 삽입합니다. 이렇게 만든 template literal은 문자열로서 취급됩니다. const myPet = 'Welsh Corgi'; console.log(`I own a pet ${myPet}.`); // I own a pet Welsh Corgi. Interpolation은 코드의 가독성을 높이므로, 만들어질 string의 모습을 누구나 쉽게 알 수 있다는 장점이 있습니다. Conditional statement 다음은 Javascript에 존재하는 몇 가지 조건문들의 문법입니다. Syntax of If statement if (condtion) { codeblock } else if (condition) { codeblock } else if (condition) { codeblock } else { codeblock } Syntax of ternary operator (condition) ? (codeblock when true) : (codeblock when false); Syntax example of switch statement let dog = 'Welsh Corgi'; switch (dog) { case 'Golden Retriever': console.log('Golden Retriever, bow wow!'); break; case 'Dachshund': console.log('Dachshund, bow wow!'); break; case 'Welsh Corgi': console.log('Welsh Corgi, bow wow!'); break; default: console.log('No correct dog but bow wow!'); break; } Reference Codecademy - introduction to javascript
JavaScript-Ecosystem
· 2021-07-22
<
>
Touch background to close