2023. 6. 14.`column-count`
이력서를 리액트 컴포넌트로 작성하다가 2단 처리를 어떻게 해야 되나 싶어서 알아보니 column-count
라는 프로퍼티가 있었고,
column-count: 2;
column-count: 2;
하면 되는 것이었다...
column-count: <number>;
column-count: <number>;
기본적으로 위와 같이 쓰면 되고, 이런 저런 관련 프로퍼티가 있는데,
column-width
column-count: auto;
column-width: 8rem;
column-count: auto;
column-width: 8rem;
content 분량에 따라 8rem
width
에 맞춰 자동으로 채워진다.
column-fill
column-fill: auto;
column-fill: balance;
column-fill: auto;
column-fill: balance;
auto
: 일반적으로 기대되는 분량에 따라 채워지는 동작balance
:column-count
에 지정된 대로 content를 나누어 각 column에 배치
column-gap
ㅇㅇ 그것임
column-rule
규칙인가? 싶지만 그게 아니고 각 컬럼 사이에 라인을 그어준다.
column-rule: dotted;
column-rule: solid 8px;
column-rule: solid blue;
column-rule: thick inset blue;
column-rule: dotted;
column-rule: solid 8px;
column-rule: solid blue;
column-rule: thick inset blue;
이런 느낌.
column-span
특정 요소가 cols 사이를 가로지르게 만들 수 있다. 아래는 mdn의 예시.
2023. 6. 13.Colors
JS yellow: #F0DC4E
TC39 orange: #FC7C00
npm red: #EA2039
2023. 6. 10.Custom Nerd Fonts
nvim등 터미널 환경에선 nerd fonts(기존 폰트에 아이콘 등 각종 UI용 문자를 추가한 폰트)를 쓰는 게 편한데 유료 폰트이거나 폰트 취향이 너무 마이너인 경우 직접 만들써야 함. 귀찮아서 한동안 걍 Iosevka 쓰다가 어제 잠깐 의욕이 생겨서 알아보니 같은 곳에서 제공하는 Font Patcher를 사용하면 되는 것이었고, 이런저런 실행 방법을 제공하는데 그 중 docker로 하는게 젤 간단했음.
docker run --rm \
-v /path/to/original/font:/in \
-v /path/to/patched/font:/out \
nerdfonts/patcher \
--progressbars \
--adjust-line-height \
--fontawesome \
--fontawesomeextension \
--fontlogos \
--octicons \
--codicons \
--powersymbols \
--pomicons \
--powerline \
--powerlineextra \
--material \
--weather
docker run --rm \
-v /path/to/original/font:/in \
-v /path/to/patched/font:/out \
nerdfonts/patcher \
--progressbars \
--adjust-line-height \
--fontawesome \
--fontawesomeextension \
--fontlogos \
--octicons \
--codicons \
--powersymbols \
--pomicons \
--powerline \
--powerlineextra \
--material \
--weather
위의 /path/to/original/font
, /path/to/patched/font
는 폴더이고 original의 경우 그 안에 폰트 파일이 들어있으면 된다. 작업이 완료되면 patched 폴더 안에 생성된 폰트 파일을 설치해서 쓰면 됨.
Before:
After:
Links
2023. 6. 10.Track Awesome Eslint Updates Daily
2023. 6. 8.Meditation
- 명상의 핵심은 퉁쳐서 말해 현재(구체적으로는 호흡)에 집중하고 떠오르는 생각을 인지한 채로 engage하지 않는 것.
- 의학적으로 유의미한 명상의 실행 유무에 따른 차이가 나타나는 데는 대략 8주 정도가 걸린다고 함.
- 그리고 세션당 20분 이상(정확히는 17분 +) 지속시 확실한 의학적 효과가 관찰된다고 함.
- HPA Axis(시상하부-뇌하수체-부신 축)가 명상으로 인해 'shutdown' 되는데 그 정도의 시간이 걸리기 때문.
- 의학적 효과는 HPA Axis 활성화도/코티졸 농도 정상화?라고 퉁쳐서 말할 수 있음.
- 코티졸 농도가 높으면 백혈구 양이 증가하고 그로 인해 자가면역질환이 발생하는데, 거기에 명상이 직접적으로 도움을 줄 수 있음.
- 명상을 하다가 조는 것은 매우 일반적인 현상.
- 명상을 하다가 자꾸 딴 생각을 하는 것도 매우 자연스러운 현상.
- 명상 state를 '유지'하는 것보다 다시 명상 state로 돌아오는 행동이 '반복'되는 것이 더 중요할 수 있음.
2023. 6. 5.`URL` object
const url = new URL('https://sehyunchung.dev')
url.toString() // 'https://sehyunchung.dev/' <- trailing slash 생기는 것에 유의
url.pathname = 'post/1'
url.toString() // 'https://sehyunchung.dev/post/1' <- trailing slash 없음
url.searchParams.set('key', 'value')
url.toString() // 'https://sehyunchung.dev/post/1?key=value'
const url = new URL('https://sehyunchung.dev')
url.toString() // 'https://sehyunchung.dev/' <- trailing slash 생기는 것에 유의
url.pathname = 'post/1'
url.toString() // 'https://sehyunchung.dev/post/1' <- trailing slash 없음
url.searchParams.set('key', 'value')
url.toString() // 'https://sehyunchung.dev/post/1?key=value'
// console.log(url)
{
hash: ""
host: "sehyunchung.dev"
hostname: "sehyunchung.dev"
href: "https://sehyunchung.dev/post/1?key=value"
origin: "https://sehyunchung.dev"
password: ""
pathname: "/post/1"
port: ""
protocol: "https:"
search: "?key=value"
searchParams: URLSearchParams {size: 1}
username: ""
}
// console.log(url)
{
hash: ""
host: "sehyunchung.dev"
hostname: "sehyunchung.dev"
href: "https://sehyunchung.dev/post/1?key=value"
origin: "https://sehyunchung.dev"
password: ""
pathname: "/post/1"
port: ""
protocol: "https:"
search: "?key=value"
searchParams: URLSearchParams {size: 1}
username: ""
}
url.pathname = ''
url.toString() // "https://sehyunchung.dev/"
url.pathname = 1
url.toString() // "https://sehyunchung.dev/1"
url.pathname = null
url.toString() // "https://sehyunchung.dev/null" ?!
url.pathname = undefined
url.toString() // "https://sehyunchung.dev/undefined" ?!
url.pathname = ''
url.toString() // "https://sehyunchung.dev/"
url.pathname = 1
url.toString() // "https://sehyunchung.dev/1"
url.pathname = null
url.toString() // "https://sehyunchung.dev/null" ?!
url.pathname = undefined
url.toString() // "https://sehyunchung.dev/undefined" ?!
2023. 6. 3.🫠 Hydration Mismatch 🫠 (2)
요기서 이것 저것 해봤는데 다 별로인 것 같다. 왜냐면 다 hydration mismatch를 해결하는 게 아니고 피해가는 것이기 때문인듯...
그러니까 애시당초 'mismatch'가 발생하는 건
- client state가 client에서만 액세스 가능한 곳에 persist 되어있어서
- server에서 액세스가 안되니까
- UI 상태가 달라질 수 밖에 없다.
인데 그렇다면
- client state persist를
- server에서 액세스 가능한 곳에 하면 되는 것잉게롱.
그래서 좀 손이 가지만 걍 이렇게 해봤는데,
- persist엔
cookie
를 사용한다. - 왜냐면 쿠키는 서버 컴포넌트에서 읽기 가능이므로. 암튼 그래서 플로우는,
- 서버 컴포넌트에서
cookie
를 읽어서 고 안에 들어있는 persisted state를 가져온다. - mismatch가 발생하는 클라이언트 컴포넌트에 위 state를 넘겨줄 prop을 하나 뚫는다.
- 그리고 같은 클라이언트 컴포넌트에
useState
로 local state를 하나 만드는데, - 만들면서 initialState로 위 서버 컴포넌트에서 받아온 prop을 넘겨준다.
useEffect
를 하나 추가해서- 클라이언트 컴포넌트가 쓰고 있는 persisted store을 local state에 묶어준다.
- 깅까 대략
export function useSyncedState<T>(clientState: T, serverState?: T) { const [state, setState] = React.useState<T>(serverState ?? clientState) React.useEffect(() => { setState(clientState) }, [clientState]) return state }
export function useSyncedState<T>(clientState: T, serverState?: T) { const [state, setState] = React.useState<T>(serverState ?? clientState) React.useEffect(() => { setState(clientState) }, [clientState]) return state }
- 왈료
이러면
- 어차피 서버도 클라이언트도 같은 걸 보고 있으므로 애시당초 mismatch가 아님.
- hydration이 되기 전에 이미 같은 상태의 UI가 보이므로 깜빡임 같은 게 없음.
근데:
- 서버 호출을 해야 됨
- 그래서 요렇게 한 컴포넌트가 들어있는 페이지는 static export가 안됨
2023. 6. 3.github issue는 discussion이 될 수도 있고 다른 repo로 갈 수도 있는 엄청난 존재이다
-
심지어 다른 repo로 갔다가 다시 돌아올수도 있다
-
근데 discussion이 되어버리면 다시 issue로 돌아올 수는 없다
- 하지만 discussion이 되어버린 자신을 복제해 새 issue를 만들 순 있다
-
cms를 issues에서 discussions로 옮기는 것은 어떨까 하여 알아보다 알게 됨.
2023. 6. 2.🫠 Hydration Mismatch 🫠 (1)
이런 저런 이유로 클라이언트 상태를 localStorage
등에 persist 하고 있을 경우 server/client mismatch가 발생할 수 밖에 없는데 그래서 서버에서 프리렌더가 안되게 하려면 아래와 같은 난리 법석이 필요.
useState
+useEffect
function Comp() { const storeState = useStoreState() ^^^^^^^^^^ 1) 요걸 그냥 쓰면 💣 인 경우, const [state, setState] = React.useState() React.useEffect(()=>{ setState(storeState) },[]) // 2) 이런 난리 법석 후에 return <div>{state}</div> // 3) 이러면 통과
function Comp() { const storeState = useStoreState() ^^^^^^^^^^ 1) 요걸 그냥 쓰면 💣 인 경우, const [state, setState] = React.useState() React.useEffect(()=>{ setState(storeState) },[]) // 2) 이런 난리 법석 후에 return <div>{state}</div> // 3) 이러면 통과
useMounted
+return null
// 0) 일단 이런 난리 법석을 만들어두고 const useMounted = () => { const [m, sM] = React.useState(false) // ^^^^^ 귀찮아서 대충 씀 React.useEffect(()=>{ sM(true) }, []) return m }
// 0) 일단 이런 난리 법석을 만들어두고 const useMounted = () => { const [m, sM] = React.useState(false) // ^^^^^ 귀찮아서 대충 씀 React.useEffect(()=>{ sM(true) }, []) return m }
function Comp() { const storeState = useStoreState() ^^^^^^^^^^ 1) 요걸 그냥 쓰면 💣 인 경우, const mounted = useMounted() if (!mounted) return null // 2) 이러고 나서 return <div>{storeState}</div> // 3) 이러면 통과
function Comp() { const storeState = useStoreState() ^^^^^^^^^^ 1) 요걸 그냥 쓰면 💣 인 경우, const mounted = useMounted() if (!mounted) return null // 2) 이러고 나서 return <div>{storeState}</div> // 3) 이러면 통과
next/dynamic
+{ ssr: false }
☜ 이게 기분이 제일 덜 나쁜듯하지만 캐치가 하나 있는데,const Comp = dynamic(() => import('path/to/comp'), { ssr: false }); // 젤 간단?
const Comp = dynamic(() => import('path/to/comp'), { ssr: false }); // 젤 간단?
Comp
는 무조건export default
여야 함.대신const Comp = dynamic(() => import('path/to/comp').then(mod => mod.Comp), { ssr: false }) // ^^^^^^^^^^^^^^^ 이러면 💣
const Comp = dynamic(() => import('path/to/comp').then(mod => mod.Comp), { ssr: false }) // ^^^^^^^^^^^^^^^ 이러면 💣
loading
으로 서스펜스 간지를 낼 수 있음(...)물론 next 한정이지만요...const Comp = dynamic(() => import('path/to/comp'), { ssr: false, loading: () => <Sekeleton /> }); // 이 가능
const Comp = dynamic(() => import('path/to/comp'), { ssr: false, loading: () => <Sekeleton /> }); // 이 가능
이거 다 별루고... 로 시작하는 글을 한참 쓰고 있었는데 브라우저 꺼져서 날아감...
2023. 6. 2.`blurDataUrl` 만들기
갯츠비는 알아서 해주지만 넥스트는 해줘야 되기 때문에...
import { decode, encode } from "blurhash"
import sharp from "sharp"
const loadImageData = async (src: string) => {
const response = await fetch(src)
if (!response.ok)
throw new Error(
`Failed to load image: ${response.status} ${response.statusText}`
)
const imageBuffer = await response.arrayBuffer()
const { data, info } = await sharp(imageBuffer)
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true })
return {
data: new Uint8ClampedArray(data),
width: info.width,
height: info.height,
}
}
export const encodeImageToBlurhash = async (imageUrl: string) => {
const { data, width, height } = await loadImageData(imageUrl)
return encode(data, width, height, 4, 4)
}
export const blurhashToBase64 = async (
blurhash: string,
width: number,
height: number
) => {
const pixels = decode(blurhash, width, height)
const webp = sharp(Buffer.from(pixels), {
raw: { width, height, channels: 4 },
}).webp()
const dataString = (await webp.toBuffer()).toString("base64")
return `data:image/png;base64,${dataString}`
}
export const generateBlurDataUrl = async (
imageUrl: string
): Promise<string | undefined> => {
try {
const blurhash = await encodeImageToBlurhash(imageUrl)
return await blurhashToBase64(blurhash, 4, 4)
} catch (error) {
console.error(error)
return undefined
}
}
import { decode, encode } from "blurhash"
import sharp from "sharp"
const loadImageData = async (src: string) => {
const response = await fetch(src)
if (!response.ok)
throw new Error(
`Failed to load image: ${response.status} ${response.statusText}`
)
const imageBuffer = await response.arrayBuffer()
const { data, info } = await sharp(imageBuffer)
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true })
return {
data: new Uint8ClampedArray(data),
width: info.width,
height: info.height,
}
}
export const encodeImageToBlurhash = async (imageUrl: string) => {
const { data, width, height } = await loadImageData(imageUrl)
return encode(data, width, height, 4, 4)
}
export const blurhashToBase64 = async (
blurhash: string,
width: number,
height: number
) => {
const pixels = decode(blurhash, width, height)
const webp = sharp(Buffer.from(pixels), {
raw: { width, height, channels: 4 },
}).webp()
const dataString = (await webp.toBuffer()).toString("base64")
return `data:image/png;base64,${dataString}`
}
export const generateBlurDataUrl = async (
imageUrl: string
): Promise<string | undefined> => {
try {
const blurhash = await encodeImageToBlurhash(imageUrl)
return await blurhashToBase64(blurhash, 4, 4)
} catch (error) {
console.error(error)
return undefined
}
}
여러가지 더 있다.