0%

주요 장점

SSR+CSR의 혼합 사용

파일 베이스 라우팅

기존 리액트의 경우 url을 변경하지만 서버에 요청을 보내진 않는다. (SPA)

리액트 라우터는 이를 제공해주긴 하지만 컴포넌트에 계속 Router를 감싸줘야하고 이는 다른 페이지에도 동일한 코드를 계속 넣어줘야한다.

코드 대신 페이지와 루트 정의를 폴더와 파일로 한다. 이로 인해 코드가 줄고 라우팅의 의미를 더 명료하게 나타낼 수 있다. 중첩 라우트, 동적 라우트 지원

백엔드 코드를 쉽게 넣을 수 있다.

Next.js 실전 체험

☝️이 체험은 Next.js 공식문서에 따른 튜토리얼입니다. 초기 구성은 문서에서 확인해주세요

Next.js의 페이지

pages 밑에 posts라는 파일을 생성합니다. 이 아래에 firstpost.js를 아래와 같이 입력합니다.

Nextjs 계층구조
1
2
3
export default function FirstPost() {
return <h1>First Post</h1>
}

그럼 아래와 같이 라우팅이 된 것을 확인할 수 있습니다

firstPost로 라우팅되는 모습

링크 컴포넌트

링크 컴포넌트를 통해 페이지를 이동할 수 있는 링크를 달아줍니다.

import Link from 'next/link'를 하고 아래에 링크 컴포넌트를 만듭니다.

1
2
3
4
5
6
7
8
9
10
import Link from 'next/link'

/.../

<h1 className="title">
Read{" "}
<Link href="/posts/first-post">
<a>this page!</a>
</Link>
</h1>

이제 폴더 ./posts/first-post로 가서 되돌아가는 컴포넌트를 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Link from 'next/link'

export default function FirstPost() {
return (
<>
<h1>First Post</h1>
<h2>
<Link href="/">
<a>Back to home</a>
</Link>
</h2>
</>
)
}

그럼 아래와 같이 라우팅이 잘 동작하는 것을 볼 수 있습니다.

라우팅 동작

클라이언트 사이드 탐색

링크 컴포넌트는 같은 Next.js 앱에서 클라이언트 사이드 탐색이 가능합니다.

클라이언트 사이드 탐색이란 브라우저 탐색보다 빠른 자바스크립트를 사용하여 페이지 전환을 일으키는 것을 말합니다. 이를 확인하기 위해선 개발자 도구에서 배경색상을 노란색으로 하고 라우팅시 변경되지 않으면 클라이언트 사이드 탐색입니다.

이를 사용하고 싶지 않다면 Link 태그를 쓰지 않고 a태그만을 이용하여 해결할 수 있습니다.

메타데이터

메타데이터 수정을 위해선 Head컴포넌트를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Link from 'next/link'
import Head from '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를 통해 메타데이터를 수정할 수 있습니다.

타입스크립트 설치

다음 명령어를 통해 타입스크립트를 설치할 수 있습니다

1
npm install -g typescript

타입스크립트 코드 작성(greeter.ts)

1
2
3
4
5
6
7
function greeter(person) {
return "Hello, " + person;
}

let user = "Jane User";

document.body.textContent = greeter(user);

💡 확장자가 .ts이지만 아직 평범한 자바스크립트 파일입니다.

코드 컴파일

아래 명령어를 터미널에 입력하세요

1
tsc greeter.js

이 결과로 나오는 코드는 greeter.js입니다. 이제 typescript를 사용할 수 있습니다!

타입스크립트는 말그대로 타입을 표기할 수 있습니다. : string과 같이 콜론 뒤에 정하고자 하는 타입을 지정합니다.

1
2
3
4
5
6
7
function greeter(person: string) {
return "Hello, " + person;
}

let user = "Jane User";

document.body.textContent = greeter(user);

여기서 인수가 개수나 형식이 잘못되어도 컴파일은 되지만, 코드가 예상대로 동작하지 않을수도 있습니다.

인터페이스

타입스크립트에서는 새로운 타입을 정의할 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
interface Person {
firstName: string;
lastName: string;
}

function greeter(person: Person) {
return "Hello, " + person.firstName + " " + person.lastName;
}

let user = { firstName: "Jane", lastName: "User" };

document.body.textContent = greeter(user);

클래스

인터페이스를 통한 추상화가 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Student {
fullName: string;
constructor(public firstName: string, public middleInitial: string, public lastName: string) {
this.fullName = firstName + " " + middleInitial + " " + lastName;
}
}

interface Person {
firstName: string;
lastName: string;
}

function greeter(person: Person) {
return "Hello, " + person.firstName + " " + person.lastName;
}

let user = new Student("Jane", "M.", "User");

document.body.textContent = greeter(user);

💡 생성자 인수의 public을 사용하면 그 인수의 이름으로 프로퍼티를 자동생성합니다.

vs의 유용한 확장을 소개하고자 합니다.🧐 보통 코드나 중괄호에서 코드를 다 작성하였을 때, 괄호문에서 탈출하기 위해 클릭이나 다른 키보드의 조합을 통해 빠져나가야 합니다. 이를 탭키 하나로 간단히 처리할 수 있습니다.🤩

예를 들어 배열을 작성한다고 했을 때 배열 안에서 탭키를 쳤을 때 마주하는 모습은 아래와 같습니다.

1
[1, 2, 3, 'cursurRightHere']

TabOut 확장 프로그램을 깔면 다음과 같이 동작합니다.

1
[1,2,3]'cursurRightHere'

실제 확장 프로그램을 설치한 뒤, 사용해보니 여러줄에 걸쳐있는 괄호문에는 동작하지 않으니 유의하세요.🤫

1
2
3
function foo('cursor1'bar)'cursor2' {'cursor3'  'cursor4'

}

즉 괄호의 짝이 한 줄에 있는 경우 TabOut이 동작하지만 그렇지 않은 경우 원래의 Tab역할을 합니다.

중첩괄호에서도 있어도 동작하니 코드 작성할 때 유용하게 사용할 수 있을 것 같습니다. 모두 즐거운 코딩하세요!🤜🤜🤜

이 글은 원문을 바탕으로 쓴 글입니다.

이 글의 목적은 브라우저에서 자바스크립트가 어떻게 동작하는지 알려주는 것입니다.

자바스크립의 브라우저 동작 과정

각 토픽으로 들어가기 전에, 대략적인 개요를 보여주겠습니다. 이 개요엔 자바스크립트가 어떻게 브라우저와 동작하는 지 보여주고 있습니다.


JS 경계선 안의 힙과 콜스택 화살표가 Web APIs를 향해 있다. Web APIs 안에 DOM, AJAX, Timeout이 있다. 콜백 큐로 향하는 화살표가 있고 이 안에는 콜백 함수들(onClick, onDone, onLoad)존재, 이벤트 루프 화살표가 이 세 영역 사이를 돌고 있고 콜스택 큐에서 JS 경계선으로 화살표가 이어져있음.


콜 스택

자바스크립트가 싱글 스레드라는 말은 들어봤을 수 있습니다. 하지만 싱글 스레드의 진짜 의미는 무엇일까요?

자바스크립트에는 콜스택이 하나만 존재하기 때문에 한번에 하나씩 할 수 있습니다.

콜 스택은 자바스크립트가 함수의 호출지를 찾아갈 수 있도록 하는 매커니즘입니다.

스크립트나 함수가 함수를 호출하면, 이 함수가 콜 스택의 가장 위에 쌓입니다. 함수가 종료되면 인터프리터가 콜스택에서 이를 다시 제거합니다.

함수가 종료되는 경우는 return문을 만나거나 스코프의 끝에 다다랐을 경우입니다.

콜스택은 가장 상단에 쌓이기 때문에 LIFO(Last In, First Out)특성을 가집니다.

1
2
3
4
5
6
7
const addOne = (value) => value + 1;
const addTwo = (value) => addOne(value + 1);
const addThree = (value) => addTwo(value + 1);
const calculation = () => {
return addThree(1) + addTwo(2);
};
calculation();

만약 위와 같은 코드를 실행시킨다고 하면 다음과 같은 과정을 따릅니다.

  1. 파일이 로드되고 main함수가 실행됩니다. 이 main함수는 전체 파일을 실행시킨다는 것을 의미합니다. 이 함수가 콜 스택에 추가됩니다.
  2. maincalculation()을 호출하고 콜 스택에 쌓입니다.
  3. calculation()addThree()를 호출합니다. 그리고 이 함수는 콜스택에 쌓입니다.
  4. addThreeaddTwo를 호출합니다. 이 함수도 콜스택에 쌓입니다.
  5. addOne도 위와 동일한 과정을 거칩니다.
  6. addOne이 다른 함수를 호출하지 않기 때문에 함수가 종료되면 콜 스택에서 제거됩니다.
  7. addOne이 종료됨에 따라 addTwo도 종료되고 콜스택에서 사라집니다.
  8. 마찬가지로 addThree도 종료됩니다.
  9. calculation에서 addTwo를 부르게 되고 콜스택에 추가됩니다.
  10. addTwoaddOne을 호출하고 콜 스택에 쌓입니다.
  11. addOne이 콜 스택에서 제거됩니다.
  12. addTwo가 콜 스택에서 제거됩니다.
  13. addThreeaddTwo가 모두 평가되었으므로 calculation은 결과값을 계산할 수 있다. calculation이 콜 스택에서 제거됩니다.
  14. 더 이상 존재하는 코드가 없기 때문에 main도 콜 스택에서 제거됩니다.

이 글에서 main이라 작성하긴 했지만 공식적으로 이에 대한 이름은 존재하지 않습니다. 브라우저 콘솔에서는 이 함수를 anonymous라 부릅니다.

Uncaught RangeError: Maximum call stack size exceeded


Uncaught RangeError: Maximum call stack size, exceeded at b (<anonymous>:6:5) exceeded at a (<anonymous>:2:5)


이 에러 메시지에 따르면, 서로 순환 호출을 하기 때문에 콜스택의 적재 횟수를 초과하여서 생긴 에러입니다. 최대 콜 스택의 범위는 최소 만 부터 5만까지이다. 따라서 이 에러가 발생한 경우에는 코드가 무한 루프에 빠지고 있습니다.

요약하자면 콜 스택은 함수의 호출지를 추적할 수 있도록 하고 LIFO(Last In, First Out) 특성을 가지고 있습니다. 다시 말해서 스택의 가장 상단에 있는 부분이 먼저 실행됩니다.

Heap

자바스크립트의 힙은 함수나 변수를 정의할 때 객체가 정의되는 곳입니다. 이 주제는 콜 스택이나 이벤트 루프와 관련은 없지만, 자바스크립트의 메모리 할당과 관련하여 알고 싶은 사람은 이 링크를 참고하길 바랍니다.

Web APIs

자바스크립트는 싱글 스레드 기반이지만 브라우저와 동시에 실행될 수 있습니다. 이유는 브라우저가 제공하는 Web API가 있기 때문입니다.

예를 들어, 자바스크립트 인터프리터를 통해 코드를 실행시키기 위해서는 서버에서 이에 대한 응답을 받아야만 합니다. 이 때문에 웹 애플리케이션의 사용이 불가능할 수 있습니다.

이에 대한 해결책으로 웹 브라우저가 자바사크립트 코드에서 실행시킬 수 있는 API를 제공합니다. 이를 실행하는 건 브라우저가 실행합니다. 이 때문에 콜스택을 막지 않습니다.

web APIs의 또다른 장점으로는 C와 같은 낮은 레벨의 코드로 쓰여진 점입니다. 따라서 자바스크립트만으로 하기 어려운 일들을 해줄 수 있습니다.

이들은 AJAX 요청이나 DOM관리를 할 수 있습니다. 이뿐만 아니라, geo-tracking, 로컬 스토리지 접근 등이 가능합니다.

콜백 큐

web APIs의 특성 덕분에 자바스크립트 인터프리터 밖에서도 무언가를 할 수 있습니다. 하지만 예를 들어 Web API나 AJAX 요청에 대한 응답에 대해 처리를 자바스크립트로 하고 싶다면 어떻게 해야할까요?

이 때문에 콜백이 등장합니다. 이들을 활용해서, web API가 API 실행 뒤에 코드를 실행시킬 수 있습니다.

콜백이 무엇인가요? 콜백이란 다른 함수에 전해지는 함수를 말합니다. 콜백은 보통 어떤 코드가 실행된 이후에 실행됩니다. 함수를 매개변수로 해서 쉽게 콜백 함수를 만들 수 있습니다. 이들은 고차함수라고 불리기도 합니다. 콜백은 기본적으로 비동기가 아닌걸 기억하세요!

예제를 살펴봅시다.

1
2
3
4
5
6
7
const a = () => console.log('a');
const b = () => setTimeout(() => console.log('b'), 100);
const c = () => console.log('c');

a();
b();
c();

이미 나올 값을 알고 있는 사람도 있을 겁니다.

setTimeout이 실행될 동안 JS 인터프리터도 다음 구문을 해석해 나갑니다. 만약 setTimeout에서 설정된 시간이 지나고 콜 스택이 전부 비게 되면 setTimeout의 인자로 넘긴 콜백 함수가 실행됩니다.

따라서 결과는 a c b가 됩니다.

setTimeout가 종료되더라도 콜백 함수는 바로 실행되지 않습니다. 이 이유는 자바스크립트가 한번에 하나의 일만 하기 때문입니다.

자바스크립트로 작성된 콜백함수는 setTimeout의 인수로 전해집니다. 그러므로 자바스크립트 인터프리터가 이를 해석(=콜스택에 추가)해야 합니다. 이를 다시 말하면, 콜백을 실행하기 위해서는 콜 스택이 전부 빌 때까지 기다려야 합니다.

setTimeout이 web APIs를 부릅니다. API는 콜백을 콜백 큐로 이동시켜줍니다. 그리고 이벤트 루프가 콜 스택이 빌 때 콜백 큐에서 콜 스택으로 콜백 함수를 추가합니다.

콜 스택과 달리 콜백 큐는 FIFO(First In, First Out)를 따릅니다. 이는 큐에 쌓인 순서대로 실행되는 것을 의미합니다.

이벤트 루프

자바스크립트 이벤트 루프는 콜 스택이 빌 때, 콜백 큐에 있는 가장 첫번째 일을 콜 스택에 추가합니다.

다르게 말하자면 콜 스택이 비지 않는 한, 콜백 큐에 있는 어떠한 일도 실행되지 않습니다.

따라서 너무 많은 코드를 실행시키거나 콜백 큐를 가로막으면, 새로운 자바스크립트 코드가 실행되지 못하기 때문에 웹 사이트가 반응하지 않을 수 있습니다.

onscroll과 같은 이벤트 헨들러들은 이벤트가 실행될 때, 콜백 큐에 일들을 추가합니다. 이 때문에 이러한 콜백들엔 디바운스가 필요합니다. 디바운스란 매 x밀리초마다 실행되도록 하는 것입니다.

실제로 해보세요 이 코드를 브라우저 콘솔에 입력해보세요. 스크롤시 얼마나 많은 콜백이 나오는지 관찰해보세요.

1
window.onscroll = () => console.log('scroll');

setTimeout(fn, 0)

위처럼 코드를 작성하는 것의 이점은 메인 스레드를 오랫동안 막아두지 않으면서 어떠한 일을 할 수 있는 것입니다.

비동기 코드를 콜백에 넣고 setTimeout을 0ms로 지정하면 브라우저가 콜백 실행을 하기 전에 DOM업데이트와 같은 작업을 수행할 수 있습니다.

잡(job) 큐와 비동기 코드

콜백 큐에 더불어 프로미스들을 독립적으로 관리하는 큐가 있는데, 그것이 바로 잡 큐입니다.

프로미스

프로미스는 EcmaScript 2015 (or ES6)에서 처음 소개되었습니다. 바벨을 통해 사양을 낮출 수도 있습니다.

프로미스는 콜백 대신 비동기 코드를 관리할 수 있는 방법입니다. 프로미스를 통해 비동기 함수를 체이닝 함으로써 콜백헬이나 운명의 피라미드에 빠지는 것을 방지할 수 있습니다.

1
2
3
4
5
6
7
8
9
setTimeout(() => {
console.log('Print this and wait');
setTimeout(() => {
console.log('Do something else and wait');
setTimeout(() => {
// ...
}, 100);
}, 100);
}, 100)

이런식으로 코드를 작성하게 되면 옆구리가 점점 더 들어가버릴거에요😞

프로미스를 활용하면 가독성을 더 높일 수 있습니다.

1
2
3
4
5
6
7
8
9
10
// A promise wrapper for setTimeout
const timeout = (time) => new Promise(resolve => setTimeout(resolve, time));
timeout(1000)
.then(() => {
console.log('Hi after 1 second');
return timeout(1000);
})
.then(() => {
console.log('Hi after 2 seconds');
});

여기서 async/await를 활용하면 더 가독성이 좋아집니다.

1
2
3
4
5
6
7
8
const logDelayedMessages = async () => {
await timeout(1000);
console.log('Hi after 1 second');
await timeout(1000);
console.log('Hi after 2 seconds');
};

logDelayedMessages();

여기서는 프로미스의 대략적인 부분만 다뤘기 때문에 더 세부적인 내용이 궁금하신 분은 MDN을 참고하세요

프로미스의 올바른 사용법

프로미스는 콜백과 다르게 그들만의 큐를 가지고 있습니다. 잡 큐는 프로미스 큐라고도 알려져있습니다. 그리고 이 큐는 콜백 큐보다 더 높은 우선 순위를 가지고 있습니다. 따라서 콜백 큐보다 우선적으로 콜 스택에 쌓습니다.

예시를 보겠습니다.

1
2
3
4
5
6
7
8
9
console.log('a');
setTimeout(() => console.log('b'), 0);
new Promise((resolve, reject) => {
resolve();
})
.then(() => {
console.log('c');
});
console.log('d');

이 예제를 보면 프로미스 큐가 콜백 큐보다 더 높은 우선순위를 가지기 때문에 출력은 a d c b로 출력됩니다.

이 글의 원문을 바탕으로 재해석한 글입니다.

min()

min() 사용 목적: 최댓값의 한계를 설정할 수 있다.

width(80ch, 100vw)의 경우 80인치와 뷰포트의 너비를 비교하여 더 작은 값을 선택한다.

.container클래스를 활용한 예제

min()함수 이내에는 계산식을 포함할 수 있다. 따라서 아래와 같이 코드를 작성할 경우, 적절한 좌우 여백을 줄 수 있다.

1
2
3
.container {
width: min(80ch, 100vw - 2rem);
}

위의 코드의 경우 .container클래스를 가진 요소는 80인치까지 너비를 가질 수 있으며, 뷰포트가 줄어들 경우 양쪽 너비 1rem의 요소를 만들 수 있다.

다른 예시로는 아래 css 코드를 들 수 있다.

1
2
3
4
5
.container {
width: min(40ch, 100% - 2rem);
margin-right: auto;
margin-left: auto;
}

여기서 ch 단위는 글꼴과 관련된 스타일이 모두 적용된 0의 너비를 이야기한다. 이를 활용하면 한 줄에 들어가는 문자의 수를 가늠할 수 있다. 따라서 어떠한 문서를 읽을 때, 좀 더 나은 경험을 제공할 수 있다.

장점 미디어 쿼리 없이도 반응형을 제작할 수 있다.

min()을 통한 반응형 요소

댓글이나 피드옆에 사용자 프로필이 있는 경우를 생각해보자. 만약 이들의 크기가 화면의 크기에 따라 유동적으로 바뀌어야 할 때 min을 활용할 수 있다. min(64px, 15%, 10vw)를 사용하면 이 요소는 min안에 적힌 3개의 값 모두를 넘어서지 못한다.

이 min함수가 사용된 요소는 64px를 절대 넘어서지 못함과 동시에 최소 15%또는 10vw의 사이즈를 가진다.

min()을 사용한 다른 특성

min()background-size에도 활용될 수 있다. 어떠한 배경 이미지가 유동적으로 확장되고자 할 때 이에 대한 경계를 min()이 지정할 수 있다.

1
2
3
4
.background-image {
background: #1F1B1C url(https://source.unsplash.com/RapCPd_mJTU/800x800) no-repeat center;
background-size: min(600px, 100%);
}

이 예시를 보면 이미지가 600px을 넘지 않을 것을 보증한다. 그리고 600px보다 작은 경우에는 알아서 크기를 줄인다.

max()

max는 반응형 요소에서 최소값의 경계를 지정한다.

max()의 경우 min()의 반대다.

max()로 문맥상 여백 주기

Web Content Accessibility Guidelines (WCAG) Success Criterion 1.4.10에 따르면 사이트를 최대 400%까지 확대할 수 있어야한다. 이때 pxrem은 표준에 맞지 않는 단위이다.

만약에 1280px사이즈의 데스크톱에 400% 줌을 준다면 뷰포트 너비가 320px로 줄어든다. 이때 모바일에서 이를 한다면, 방향은 가로로 계속 유지된다. 이러한 뷰포트의 축소는 읽기와 상호작용 영역이 감소했음을 의미한다. 더하여 휴대폰에서는 적합한 크기가 확대된 창에서는 훨씬 커보일 수 있다.

이때 쓸 수 있는 것이 max()이다. 원문의 저자는 작은 여백에는 rem단위를 선호한다. 하지만 영역 간의 큰 여백을 줘야하는 경우에는 아래와 같은 코드를 사용한다. 이렇게 하면 뷰포트에 따라 조정이 된다.

1
2
3
.element + .element {
margin-top: max(8vh, 2rem);
}

뷰포트의 너비가 큰 경우 8vh가 쓰이고 그것보다 작거나 확대된 창에서는 2rem이 적용된다.

max를 활용한 iOS 브라우저 자동 줌인 방지

iOS에선 16px보다 작은 입력창을 가지고 있으면 자동으로 줌인이 된다. 이 떄 아래와 같은 코드를 사용하면 이 문제를 해결 할 수 있다.

1
2
3
input {
font-size: max(16px, 1rem);
}

max()를 통해 어느 값이던 수용할 수 있고, 최소 16px을 유지할 수 있게 된다.

max()를 활용한 포커싱 아웃라인

max를 활용하면 포커싱 아웃라인에 대한 상대적인 크기를 설정할 수 있다. 아래 코드를 통해 아웃라인의 최소 너비를 2px로 설정하고, em값에 따라 너비가 상대적으로 조절될 수 있다.

1
2
3
4
5
6
7
8
9
10
a {
--outline-size: max(2px, 0.08em);
--outline-style: solid;
--outline-color: currentColor;
}

a:focus {
outline: var(--outline-size) var(--outline-style) var(--outline-color);
outline-offset: var(--outline-size);

max()와 타겟 사이즈

타겟 사이즈란 말은 WCAG Success Criterion (SC) 2.5.5에 나오며 타겟은 포인터 이벤트를 받을 수 있는 것을 말한다. 곧 나올 WCAG 2.2버전에서는 이런 타겟 사이즈를 최소 44px로 설정할 것을 권고한다.

이 때도 max()를 활용할 수 있다. 만약 프로필을 눌러 프로필 페이지로 이동해야하는 경우 프로필 버튼에 다음과 같은 스타일링을 줄 수 있다.

1
2
3
4
.icon-button {
width: max(44px, 2em);
height: max(44px, 2em);
}

aspect-ratio의 보완책 max()

aspect-ratio를 지원하지 않는 브라우저에서 max()에 높이를 지정하는 방식으로 비슷하게 흉내낼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
img {
/* Fallback for `aspect-ratio` of defining a height */
height: max(18vh, 12rem);
object-fit: cover;
width: 100%;
}

/* When supported, use `aspect-ratio` */
@supports (aspect-ratio: 1) {
img {
aspect-ratio: var(--img-ratio);
height: auto;
}
}

이 글의 원문을 바탕으로 재해석한 글입니다.

CSS에는 수학적으로 수치를 계산할 수 있는 4가지 함수를 제공한다(IE에선 지원하지 않는다). 자주 쓰이지 않을 순 있지만, 그레이디언트와 색깔에 관함 함수 또는 CSS의 커스텀 프로퍼티들과 조합할 때 유용하다. 그래서 아래 4가지 함수와 예시를 함께 들어보려 한다.

calc()

calc() 사용 목적 : 단위를 가진 값들의 사칙연산을 가능하도록 하는 것이다. 그리고 단위를 혼합해서도 사용할 수 있다.

예를 들어 뷰포트에서 상단의 네비게이션 영역을 뺀 값을 높이로 설정하고 싶다면 다음과 같이 쓸 수 있다.

1
2
3
.content{
height: calc(100vh - 60px);
}

사용하는 디바이스에 따라 100vh가 동적으로 결정되므로, 디바이스에 따라 높이가 달라진다.

calc의 장점은 하드 코딩을 피하고, 인라인 스타일을 추가하는 자바스크립트 로직이 줄어드는 것이다.

calc()를 사용한 컬러팔레트 생성

hsl()과 같이 사용하면 hue, saturation, lightness에 css varaible을 넣음으로써 더 응집도 있는 컬러 팔레트를 완성시킬 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.colors {
--base-hue: 140;
--saturation: 95%;
--lightness: 80%;
--rotation: 60;

color: #222;
text-align: center;
}

.color {
padding: 0.25rem;
background-color: hsl(var(--hue), var(--saturation), var(--lightness));
}

.color1{
--hue: calc(var(--base-hue));
}

.color2 {
--hue: calc(var(--base-hue) + var(--rotation));
}

.color3 {
--hue: calc(var(--base-hue) + var(--rotation) * 2);
}

clamp()

clamp()의 사용 목적 : 가능한 값의 범주를 지정한다.

clamp()는 3가지 매개변수(최소값, 이상적인 값, 최고값)를 받는다. 이 함수는 fluid typography에서 유용하다. fluid typography란 스크린 크기에 비례하여 폰트사이즈가 유동적으로 변하는 것이다.

따라서 font-size값에 clamp()를 적용하면 이를 만들 수 있다. 이 함수를 사용하면 폰트 사이즈 크기가 너무 커져 제목이 2줄이 되지 않도록 하고 뷰포트의 사이즈를 과도하게 차지하지 않게 할 수 있다.

1
2
3
h1 {
font-size: clamp(1.75rem, 4vw + 1rem, 3rem);
}

clamp()를 이용한 반응형 패딩

또 다른 예제로는 반응형 패딩이 있다. 패딩을 퍼센티지로 사용할 경우에는 요소의 너비를 가지고 계산한다. 이와 비슷하게 vw의 개념도 생각해 볼 수 있다. 아래의 코드를 보면 .element를 가진 요소들은 1rem보다 작은 패딩값을 가지지 않게 된다.

1
2
3
.element{
padding: 1.5rem clamp(1rem, 5%, 3rem);
}

이를 활용하면 미디어 쿼리를 사용하지 않고도 반응형 패딩을 만들 수 있다.

스토리북 작성 방법

리액트와 함께 스토리북을 작성해보자. 스토리북을 작성하면 코드 작성에 따라 문서가 자동으로 생성되기 때문에 협업을 하기 편리하다.

일단 컴포넌트를 아래와 같이 작성하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { string, oneOf } from 'prop-types';

const HomeLink = ({as:Comp, lang, className, children, ...restProps}) =>{
return (<>
<Comp lang = {lang} className = {className}><a href ="www.google.co.kr">{children}</a></Comp>
</>);
};

HomeLink.defaultProps = {
as : 'h1',
lang: 'en',
className: string
}

HomeLink.propTypes = {
/** 제목 요소를 설정합니다. */
as: oneOf(['h1','h2','h3','h4','h5','h6']),
/** 언어를 지정할 수 있습니다. (스크린 리더 음성 변경 됨) */
lang: oneOf(['en', 'ko']),
/** 클래스 이름을 추가할 수 있습니다. */
className: string,
}

export default HomeLink;

여기서 기본값인 defaultProps를 작성하고 props에 대한 타입을 지정하기 위해 propTypes를 설정한다. 타입스크립트의 경우 이부분은 생략할 수 있다. 만든 컴포넌트는 다시 내보낸다.

이제 만든 파일에 대한 스토리를 작성한다. 스토리 파일은 아래와 같이 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import HomeLink from "./HomeLink";
import { Logo } from "components";

export default {
title: 'Components/UI/HomeLink',
component: HomeLink,
parameters:{
docs: {
description:{
component: '홈 링크 입니다.'
}
}

},
argTypes: {
children:{
table: {disable: true},
}
}
};

const Template = (args) => <HomeLink {...args}/>;

export const Img = Template.bind({});
Img.args = {as: 'h1', children:<Logo></Logo>};
Img.storyName = '홈 링크 (이미지)';


export const Text = Template.bind({});
Text.args = {as: 'h1', children:'111'};
Text.storyName = '홈 링크 (텍스트)';

같은 경로에 HomeLink.stories.js를 생성하고 스토리 파일을 작성한다. export default안에 있는 블럭은 각각 다음과 같다.

  • title : 스토리 파일 내에서 경로 설정
  • componenet : 해당 컴포넌트에 대한 설명
  • table : 스토리북 내에서 props의 표시 유무를 설정하는 값. 기본적으론 false이며 이 값이 true로 변경하는 경우 스토리북 내에서 해당 props가 보이지 않게 된다.

props값에 따라 컴포넌트가 어떻게 변화하는지 보고 싶은 경우에는 템플릿을 작성하여 이를 보여줄 수 있다. Template를 선언하고 여기에 객체를 바인딩하여 내가 전달하고자 하는 props를 매개변수로 전달한다. 이 때 storyName을 통해 스토리북 내에서 표시될 이름을 설정할 수 있다.

링크 타입 : noreferrer

noreferrer<a>, <area>, <form>rel 속성에 쓰는 속성 값이다. 타겟 브라우저로 이동할 때 원 브라우저에 대한 정보(Referer)를 제공하지 않는다. 레퍼러를 제공하지 않기 때문에 새 탭으로 인한 보안 문제가 해결된다. 이런 면에서는 noopener가 쓰인 것과 같은 효과를 볼 수 있다.

링크 타입: noopener

noopenera, area, form태그의 rel속성에 들어가는 속성 값이다. 이 값은 새로운 창이 열려있을 때, 기존에 브라우징 되어있는 요소에 접근할 수 없도록 한다.

A브라우저에서 링크를 타고 B브라우저를 새탭에서 열었을 경우 Window.opener라는 객체가 생기는데, 이걸 통해 B브라우저가 A브라우저를 악의적으로 동작하게 만들 수 있기 때문에 noopener는 이 객체를 생성하지 않고 널값을 반환한다.

이 값은 특히 신뢰할 수 없는 사이트에 들어갈 때 유용하다. 이 값을 사용하면 Window.opener를 통해서 현재 브라우저에 접근할 수 있는 통로를 차단한다. (이 값을 사용한다는 기본 전제는 RefererHTTP 헤더를 사용한다는 것이다. 리퍼러는 사용자가 어디에서 새 브라우저로 왔는지 url에 보여준다. 이 정보가 명확히 있기 때문에 Window.opener에서 조작할 수 있는건데, noreferrer를 사용한다면 애초에 어디서부터 왔는지 보이지 않기 때문에 noopener를 사용할 이유가 없어진다.)

noopener를 사용할 경우 target속성에 기존 속성값이 아닌 임의의 값을 넣으면 _blank처럼 여겨져서 계속 새로운 창을 띄운다.(원래는 프레임 내임으로 탭이 하나 생성되고 링크를 여러번 타도 그 창을 삭제하지 않는 한 새로운 탭이 생성되지 않는다.)

현재 target="_blank"<a>태그에 사용하면 이 값이 암묵적으로 적용된다.

문제정의


가사에 사용된 모든 단어들이 담긴 배열 words에서 queries에 해당되는 단어가 몇 개가 있는지 반환하는 문제이다. queries에는 와일드 카드 ?가 하나 이상 존재하며, 이 와일드 카드는 문자의 앞부분을 차지하거나 뒷부분만을 차지한다(fr?do와 같은 형식이 없다.).

문제 링크

문제풀이


trie 자료구조를 이용하여 문제를 풀 수 있다. 이번에는 다른 분의 핵심 아이디어를 듣고 코드로 구현하였다. 코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def solution(words, queries):
class Node:
def __init__(self, value=None):
self.value = value
self.child = dict()
self.len_dict = dict()

class Trie:
def __init__(self):
self.root = Node()

def add(self, value:str):
node = self.root
for c in value:
# add dictionary for wildcard
if len(value) not in node.len_dict:
node.len_dict[len(value)] = 0
node.len_dict[len(value)] += 1
if c not in node.child: # There is no child
new_node = Node(c)
node.child[c] = new_node
node = new_node
else:
node = node.child[c]
node.child['*'] = None

def search(self, queri):
node = self.root
for c in queri:
if c == '?':
return node.len_dict[len(queri)] if len(queri) in node.len_dict else 0
if c in node.child:
node = node.child[c]
else:
return 0

return 0

# def print(self):
# node = self.root
# queue = []
# queue.append(node)
# while queue:
# root = queue[0]
# del queue[0]

# if root.value is not None:
# print(root.value, end = " ")

# if '*' in root.child:
# print('*')
# continue

# for node in root.child.values():
# queue.append(node)

reversed_words = [word[::-1] for word in words]

trie = Trie()
for word in words:
trie.add(word)

reversed_trie = Trie()
for word in reversed_words:
reversed_trie.add(word)

answer = []

for queri in queries:
if queri[0] == '?':
answer.append(reversed_trie.search(queri[::-1]))
else:
answer.append(trie.search(queri))
return answer

일단 Trie에 대해서 알아야 하는데 이에 대한 링크를 첨부해 두었으니 참고하면 좋을 것 같다. 이에 대해 안다고 가정하고 설명을 진행한다.

이 문제에서 제한 시간내에 문제를 풀기 위한 핵심 요소는 와일드 카드의 사용이다. 문자 사이에 와일드 카드가 올 일은 없기 때문에 만약 접미사가 와일드 카드이면 더 이상 Trie를 탐색하지 않도록 해야 문제가 풀린다.

이를 풀기 위해 len_dict를 생성하였다. len_dict는 해당 노드에서 뻗어나가는 단어들을 길이에 따라 몇 개의 단어가 있는지 알려주는 딕셔너리이다. 문제 테스트 케이스에 대해 len_dict를 그려보자면 다음과 같다.

Trie와 len_dict

그림을 보면 root에 5글자를 가진 단어 5개 6글자를 가진 단어 1개를 표시해두고 아래로 뻗어 나가면서 각자의 위치에 자신의 하위 위치에 있는 단어들의 개수를 길이에 따라 저장해둔 것을 볼 수 있다. 이런식으로 했을때 만약 fro??와 같은 쿼리가 들어오면 o까지만 순회를 하고 5를 key값으로 하는 value 3을 리턴해주면 된다.

이렇게 하면 해결하지 못하는 케이스가 여전히 있는데 바로 와일드 카드가 앞쪽에 붙는 경우다. 이렇게 하면 모든 단어를 순회해봐야 알 수 있게 되기 때문에 역시나 효율성에서 애를 먹는다. 이를 해결하는 방법은 간단한다. 단어를 거꾸로 하여 새로운 Trie를 만들고 쿼리도 역으로 하여 search를 진행하면 된다.

시간복잡도를 계산하자면, words의 길이를 n, 개수를 m이라 하면 trie를 생성하는데에 \(O(nm)\)이 든다. 이를 만들고 쿼리를 진행하므로 queries의 길이를 q라하고 개수를 p라하면, \(O(pq)\)만큼 소요된다. 따라서 최종 시간복잡도는 \(O(nm) + O(pq)\)이다.

너무 시간복잡도가 어마어마하게 느껴질 수도 있지만, 와일드카드와 단어간의 중첩이 있어 실질적인 시간은 좀 더 짧다고 생각한다.

테스트


테스트 화면