github 404 page clone

  • github 404 페이지를 클론해보았다
  • 마우스 움직임에 따라 원근감 있게 움직이는 이미지들이 재밌었다
  • 전에 3d 카드 이펙트 만들면서 perspective css 속성을 사용해보았는데, 그때 사용한 코드를 거의 그대로 사용해서 만들 수 있었다
  • 3d card effect ; https://github.com/chinsun9/3d-card-effect
  • 이미지는 직접 그렸다. 물론 바닥에 대고…

참고

깃허브 블로그 느린 웹 해결하기

  • 2020.01.04 처음 느린 웹 보고서를 확인하고 그냥 방치 했는데…
  • 나날이 늘어가는 느린 URL들…

  • jsdelivr 적용 후, 지금까지!

상황

  • 구글 서치 콘솔에서 블로그에 대한 보고서를 보는데, 느린 웹 문제가 있었다
  • 어떻게 블로그를 빠르게 할 수 있는지 고민해보았다

내 환경

  • hexo 프레임워크로 블로그를 운영하고 있다
  • 블로그 글이 점점 많아지고, 글에 많은 이미지를 포함하는 글도 있다

해결 : jsdelivr 적용

  • jsdelivr는 npm과 github에서 사용할 수 있는 무료 CDN이다
  • 오픈소스 프로젝트를 위한 CDN이라고 한다
  • jsdelivr는 아무런 설정 없이 누구나(npm, github 사용자) 사용할 수 있는 게 장점이다

jsdelivr 사용법

https://chinsun9.github.io/images/web-dark-theme20210531/preview.png (원본)
https://cdn.jsdelivr.net/gh/chinsun9/chinsun9.github.io@master/images/web-dark-theme20210531/preview.png (CDN 적용)

hexo에서 적용

  • 포스트를 작성할 때 cdn을 적용한 url을 적어줘도 상관은 없지만,
  • 이미 작성된 글들에 대해 수행해야 하는 불편함이 있다
  • hexo 모듈을 살펴보니 node_modules/hexo/lib/plugins/filter/after_post_render/index.js에서 추가적인 작업을 해줄 수 있다는 것을 알게 되었다
hexo/lib/plugins/filter/after_post_render/index.js
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
'use strict';

module.exports = (ctx) => {
const { filter } = ctx.extend;

function lazyProcess(htmlContent) {
let rootUrl =
'https://cdn.jsdelivr.net/gh/chinsun9/chinsun9.github.io@master';
return htmlContent.replace(
/<img(.*?)src="(.*?)"(.*?)>/gi,
function (str, p1, p2) {
if (/src="data:image(.*?)/gi.test(str)) {
return str;
}
if (p2.indexOf('http') === 0) {
return str;
}
return str.replace(p2, rootUrl + p2);
}
);
}

filter.register('after_post_render', require('./external_link'));
filter.register('after_post_render', require('./excerpt'));
filter.register('after_post_render', function (data) {
data.content = lazyProcess.call(this, data.content);
return data;
});
};
  • node_modules 아래에 있는 파일을 수정한 것이라, 좋은 접근은 아닌 것 같다
  • 이런 경우에 어떻게 노드 모듈 외부에서 함수를 오버라이딩할 수 있는지…
  • 일단 나는 이렇게 해서 사용하고 있다

경과

  • 2021-03-12 이미지에 cdn 적용했다

  • 10일 후에 점점 느린 URL이 없어지고 있는 것을 확인할 수 있다

  • 지금은 느린 웹이 없다!

주의사항

  • 이미지를 수정해도 즉시 반영되지 않는다
  • 즉시 반영을 원한다면 이름을 바꿔주자

참고

CDN

  • 속도!
  • 웹프로그래밍 시간에서 bootstrap, jquery, fontawesome 등 외부라이브러리를 CDN을 통해 사용해았다
  • 데이터통신, 네트워크 관련 수업에서 CDN이 뭔지 배웠었다

CDN

  • 콘텐츠 전송 네트워크, Content Delivery Network
  • 나와 물리적으로 가까운 캐시 서버로부터 리소스를 가져온다
  • 원래대로라면 원본 리소스를 가지고 있는 서버로부터 제공받는다
  • 근데 원본 서버가 물리적으로 거리가 멀면 가져오는데 시간이 오래 걸린다
  • 원본 서버의 부하도 줄여주고 속도도 빨라지도 좋다
  • 보통 정적인 파일들(이미지, js, css)등을 CDN을 사용하여 배포한다
  • A, B가 같은 동네에 살고 있다
  • A가 asdf라는 파일을 다운로드하는데, 가장 가까운 캐시 서버에 asdf파일이 있는지 확인한다
  • 없으면 하나 위로 가서 확인하고, 반복하다가 없으면 원본 서버까지 가서 받아온다
  • 최초에 시간이 오래 걸렸지만, 이후 A가 asdf파일을 재요청했을 때 엄청 빨라졌음을 느꼈다
  • B도 asdf파일을 엄청 빨리 다운로드할 수 있었다

CDN 사용예

  • jquery, font awesome 등 라이브러리들을 보면 CDN 형태로 제공을 하고 있어서
  • head에 한 줄 넣기만 하면 해당 라이브러리의 기능을 쉽게 사용할 수 있다
  • CDN 말고 직접 배포 파일(dist, build)을 받아서 사용할 수 있지만,
  • 실제 서비스를 할 때, 내 서버에서 해당 리소스를 제공해야 하므로 서버에 부하를 줄 수 있고, 느리다
  • 라이브러리를 가져올 때, CDN을 사용했다면 이런 걱정을 할 필요가 없다

참고

web dark theme

  • 깃허브 같은 웹페이지는 내가 다크 모드를 쓰는 걸 어떻게 알고, 자동으로 다크 테마를 지원하는 걸까?

@media (prefers-color-scheme: dark)

  • 미디어 쿼리로 시스템 테마를 읽어 올 수 있다

테마

style.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
:root {
--top-bar: #dddddd;
--activity-bar: #2c2c2c;
--side-bar: #61616130;
--editor: #ffffff;
--editor-content: #000000;
--status-bar: #007acc;
}

@media (prefers-color-scheme: dark) {
:root {
--top-bar: #3c3c3c;
--activity-bar: #333333;
--side-bar: #252526;
--editor: #1e1e1e;
--editor-content: #ffffff;
--status-bar: #007acc;
}
}
  • 테마는 이런 식으로 정의했다
  • 미디어 쿼리에 걸리면 색상을 정의해둔 사용자 지정 속성들이 오버라이딩된다
  • 색상은 vscode color theme default light, dark에서 가져왔다

테마 토글 버튼

  • 시스템 테마와 별개로 웹페이지에서 테마를 토글 하고 싶을 때는 어떻게 해야 할까?
  • body 태그에 dark라는 클래스를 유무에 따라 테마를 바꿔보자
  • 테마 토글 버튼을 누르면 dark 클래스가 생기고 없어지면서 색상이 적용될 것이다

최초 접속자 테마 초기화

index.js
1
2
3
//   visit first time
const isDark = window.matchMedia('(prefers-color-scheme: dark)');
if (isDark) document.body.classList.add('dark');
  • 미디어 쿼리로는 최초 웹페이지 접속자가 어떤 테마를 사용할지 판단할 때 사용하도록 했다

토글 버튼

index.js
1
2
3
4
5
function toggleTheme() {
console.log(`toggle theme`);
document.body.classList.toggle('dark');
localStorage.setItem('theme', document.body.className ? 'dark' : 'light');
}
  • toggle은 있으면 없애고, 없으면 있게 만든다
  • toggle 후에는 현재 테마 상태를 localStorage에 저장한다
  • 이후 접속 시 localStorage에서 theme에 값이 있는지 확인하고 있으면 해당 테마로 초기화한다

chrome에서 시스템 테마 쉽게 토글 하기

  • f12 개발자 도구에서 esc key 눌러서 하단 console 창을 뛰움
  • three dot (more tools) 눌러서 Rendering 탭 오픈
  • 스크롤 중간 정도에 Emulate CSS media feature prefer-color-scheme 에서 테마 선택

참고

windows dark mode 윈도우 다크 테마

  • 다크 모드는 OS에서 설정할 수 있다
  • 최상위 (OS)에서 설정해두면 그 위에서 돌아가는 앱이 자동으로 테마를 스위치 한다
  • 깃허브 다크 모드가 크롬 프로필에 따라 풀려서 다크 모드로 설정을 하려는데
  • 시스템 기반으로 테마를 토글 하는 기능이 있음을 알게 되었다
  • 그래서 윈도우에서 다크 모드 적용을 알아보았다

윈도우 다크 모드

  • win 키를 누르고 theme를 검색하면 테마 컬러 선택 설정으로 들어갈 수 있다

  • 색 선택에서 어둡게를 선택하면
  • 다크 모드 설정 완료이다
  • 익스플로러나 브라우저를 보면 자동으로 다크 모드가 된 것을 확인할 수 있다
  • 깃허브 사이트나 깃허브 데스크톱 앱도 자동으로 다크 모드로 전환되었다
  • 앱또는 웹사이트마다 일일이 다크 모드 적용하기보다 OS 레벨에서 다크 모드를 적용하는 것이 훨씬 유용하다

react ts context

  • react에서 상위에서 하위 컴포넌트로 어떤 값을 보내 줄 때 prop으로 넘겨주게 된다
  • 이게 1, 2단계 정도면 괜찮을 텐데, 더 깊어지면 엄청 불편해진다
  • 넘기고 넘기고 넘기고 반복되는 것을 prop drilling이라고 한다
  • prop drilling으로 만들 때 긍정적인 점은 그만큼 각 컴포넌트가 느슨하게 연결되도록 만들었다는 게 아닐까..?
  • 컴포넌트가 독립성을 가지게 해주는 것도 좋지만 재사용될지 모르는 것에 너무 힘을 쓰진 말자…
  • 전역적인 state를 사용하고 싶을 때 사용할 수 있겠다
  • 리액트를 처음 배울 때 무작정 튜토리얼을 따라 하다가 redux를 배웠고,
  • 나중에 context api 만으로 충분하다는 것을 알게 되었다
  • 일단 다른 라이브러리를 설치하지 않고 충분히 가능하다는 것에 주로 사용하게 되었다
  • 사용 흐름은 redux나 context api 유사하다
  • 전역 저장소(스토어)를 생성하고, 각 컴포넌트에서는 dispatch를 통해서 상태를 업데이트할 수 있다
  • 생성, 삭제가 있다고 하면 생성 액션, 삭제 액션이 있고 dispatch 할 때 어떤 액션을 할지 정해서 업데이트를 수행한다
  • 실제 업데이트는 리듀서에서 된다. 리듀서에는 액션의 구현부?가 있다

참고 소스 코드

GlobalContext.tsx
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
/* eslint-disable no-case-declarations */
import React, { createContext, Dispatch, useContext, useReducer } from 'react';
import { Comment } from '../types';

type State = Map<number, Comment>;

const initialState: State = new Map<number, Comment>();

initialState.set(0, {
id: 0,
avatar: 'https://i.pravatar.cc/48',
username: 'string',
publishedTime: '1주 전',
content: 'hello world!',
likeCount: 999,
dislikeCount: 0,
});

type CommentAction = { type: 'ADD_COMMENT'; comment: Comment };

const CommentContext = createContext<State>(initialState);
const CommentDispatch = createContext<Dispatch<CommentAction>>(() => null);

let nextId = 0;

function reducer(state: State, action: CommentAction): State {
switch (action.type) {
case 'ADD_COMMENT':
const { comment } = action;
nextId += 1;
return new Map(state).set(nextId, { ...comment, id: nextId });

default:
throw new Error('Unhandled action');
}
}

export default function GlobalContextProvider({
children,
}: {
children: React.ReactNode;
}) {
const [state, dispatch] = useReducer(reducer, initialState);

return (
<CommentContext.Provider value={state}>
<CommentDispatch.Provider value={dispatch}>
{children}
</CommentDispatch.Provider>
</CommentContext.Provider>
);
}

export function useCommentState() {
return useContext(CommentContext);
}

export function useCommentDispatch() {
return useContext(CommentDispatch);
}
  • 컨텍스트를 만들고
index.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import GlobalContextProvider from './context/GlobalContext';

ReactDOM.render(
<React.StrictMode>
<GlobalContextProvider>
<App />
</GlobalContextProvider>
</React.StrictMode>,
document.getElementById('root')
);
  • index.tsx에서 App을 감싸든 어디든 같은 컨텍스트로 묶어줄 상위 컴포넌트를 감싸준다
App.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function App() {
const comment = useCommentState();

return (
<Main>
<Header />

<div className="contents">
{Array.from(comment).map(([id, data]) => {
return (
<div className="commentThread" key={id}>
<Comment data={data} />
</div>
);
})}
</div>
</Main>
);
}
  • 참조하여 사용할 때는 이렇게
Header.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Header() {
const commentDispatch = useCommentDispatch();

// ...

const onSubmitHandler = () => {
commentDispatch({
type: 'ADD_COMMENT',
comment,
});
};

// ...
}
  • 상태를 업데이트하고 싶으면 이런 식으로 할 수 있겠다

참고 소스 코드 저장소

참고

css 변수 사용하기

style.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
:root {
--a: pink;
--b: black;
--c: white;
}

button {
background-color: var(--a);
}

/* 변수 --a 를 오버라이딩 */
button.override {
--a: green;
background-color: var(--a);
}
  • :root는 전역 변수를 선언하는 곳이라고 생각하면 된다
  • --을 앞에 붙여야 한다
  • 대소문자를 구분한다
  • var()함수로 변수를 가져다 사용할 수 있다
  • 오버라이딩 가능하다
style.css
1
2
3
button {
background-color: var(--a, red);
}
  • var() 함수에 2번째 인자를 넣어서, 대안 값을 지정할 수 있다
  • --a가 정의되지 않았을 때, 두 번째 인자 값을 사용한다

참고

오버라이딩 vs. 오버로딩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 오버로딩 //
void f() {
// ...
}
void f(int a) {
// ...
}

// 오버라이딩 //
class Super {
void f() {
// print super
}
}

class Sub extends Super {
void f() {
// print sub
}
}

  • 예~~~전에 c++, java하면서 배운 헷갈리는 용어 정리!

react table 연습

  • 표를 포함한 간단한 웹을 만들 때 부트스트랩 템플릿 안에 들어있는 datatables를 사용하곤 했었다
  • 데이터가 쌓이지 않는 상황에서,
  • 페이지네이션 및 필터링을 이미 제공하고 있기 때문에 내가 구현할 내용이 거의 없다는 게 장점이다

상황

  • 나는 주로 리액트를 사용하는데, 이 부트스트랩 템플릿(html, js, css)을 그대로 리액트로 가져왔다
  • 이 템플릿의 종속성인 vendor 디렉터리에 있는 내용들도 public으로 옮기고,
  • 컴포넌트에서는 동적으로 이 종속성을 스크립트 태그로 로드하고
  • 이 스크립트에서 사용하는 함수를 호출하기 위해서 window 객체에 담아서 실행하고…
  • 하지만 테이블 관련 동작을 커스텀하기도 어렵고,, 좋지 못한 구조로 가는 느낌이 들었다
  • 그래서 리액트에서 사용할 수 있는 테이블 라이브러리를 찾다가 react-table을 공부해보기로 했다

따라하기

  • Codevolution React Table Tutorial을 보고 따라했다

타입스크립트로 따라하기

type

  • yarn add @types/react-table
  • 타입스크립트로 진행하려면 필요하다

useTable()

1
2
3
4
const tableInstance = useTable({
columns, // ts-err
data,
});
1
Type '({ Header: string; Footer: string; accessor: string; columns?: undefined; } | { Header: string; Footer: string; columns: { Header: string; Footer: string; accessor: string; }[]; accessor?: undefined; })[]' is not assignable to type 'Column<{ ... }>[]'.
  • 위 에러 메시지가 나온다
1
2
3
4
5
const tableInstance = useTable({
// @ts-ignore
columns,
data,
});
  • 위처럼 주석을 추가한다
  • 다른 react-table typescript example을 보니까 // @ts-ignore을 사용하는 걸 참고 했다

자동완성 안됨

1
2
3
4
5
6
7
8
9
<thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((columns) => (
<th {...columns.getHeaderProps()}>{columns.render('Header')}</th>
))}
</tr>
))}
</thead>

레포

참고