혹시 이분 인생을 이분보다 더 사랑하긴 하는 거예요? [...] 때같은 소리 하고 있네.
혹시 이분 인생을 이분보다 더 사랑하긴 하는 거예요? [...] 때같은 소리 하고 있네.
항상 부족함을 많이 느끼는데 결국엔 이 감정을 어떻게 잘 다독여서 더 보게 하느냐가 중요한 것 같다. 남은 해에는 글로 정리하는 습관을 다시 쌓아보는 것으로. 하고 싶은 것도, 해야 할 것도 많은데. 역시나 시간은 사정을 봐주지 않는다.
"마트가서 우유 하나 사고 아보카도 있으면 6개 사와" 요즘 숏폼으로도 많이 돌아다니길래 재미삼아서. 가장 먼저 코드를 작성하기 전에 요구사항을 잘 읽는다.
정리하면
명시되지 않은 상황과 조건은 다시 확인이 필요하다.
// 마트가서 우유 하나 사고 아보카도 있으면 6개 사와
function okJob1(person, place) {
person.purchase("milk", 1, place)
if (place.has("avocado")) {
person.purchase("avocado", 6, place)
}
}
function okJob2(person, place) {
person.purchase("milk", 1, place)
place.has("avocado") && person.purchase("avocado", 6, place)
}
function buggyJob(person, place) {
// 아보카도가 있으면 우유 6개 사온다는 설정은
// 코드로 봐도 좀 이상한 결정인 것 같은데
// 세상은 넓고 요구사항은 다양하니까...
person.purchase("milk", place.has("avocado") ? 6 : 1, place)
}
대략 이런 구현을 사용해서 일을 잘 정리했는지 테스트해본다.
class Location {
constructor(name, inventory) { this.name = name; this.inventory = inventory; }
has(item) { return this.inventory.includes(item); }
}
class Person {
constructor(name) { this.name = name; }
purchase(item, count, location) {
console.log(`${this.name} purchased ${count} ${item} from ${location.name}.`)
}
}
const memberOfHousehold = new Person("Spouse");
const marketWithAvocado = new Location("market", ["milk", "avocado"]);
const marketWithoutAvocado = new Location("market", ["milk"]);
okJob1(memberOfHousehold, marketWithAvocado);
// Spouse purchased 1 milk from market.
// Spouse purchased 6 avocado from market.
okJob1(memberOfHousehold, marketWithoutAvocado);
// Spouse purchased 1 milk from market.
okJob2(memberOfHousehold, marketWithAvocado);
// Spouse purchased 1 milk from market.
// Spouse purchased 6 avocado from market.
okJob2(memberOfHousehold, marketWithoutAvocado);
// Spouse purchased 1 milk from market.
buggyJob(memberOfHousehold, marketWithAvocado);
// Spouse purchased 6 milk from market.
buggyJob(memberOfHousehold, marketWithoutAvocado);
// Spouse purchased 1 milk from market.
간 김에 이것저것 장을 많이 봐서 올 것 같은데. 내일 아침은 아보카도 토스트 해야겠다.
애플워치를 한동안 사용했지만 도무지 매일 충전하는 일이 익숙해지지 않았다. 온갖 알림 덕분에 모든 것을 놓치지 않고 살게 하지만 눈 앞에 있는 일에 좀 소홀해지는 기분도 들어서, 결국엔 시계 기능만 잘하는 카시오 시계를 한동안 차고 다녔다. 그러다 미밴드에 대해 우연히 듣고는 이 시계는 좀 괜찮지 않을까 싶어서 구입했다.
알림 메시지가 가끔 잘 안온다는 얘기가 있던데 문자든 전화든 아무 알림도 안오게 설정하고 사용하고 있어서 그건 확인을 못해봤다. 조용한 웨어러블 기기로 쓴다면 전혀 부족하지 않아 만족스럽다.
3월 27일부터 30일까지 3박 4일 (+1 레드아이) 일정으로 뉴욕 여행을 다녀왔다. 그동안 가정사에 큼직한 일이 줄줄이 있어서 어딜 가지 못하다가 조금은 갑작스럽게 리프레시 여행을 결정하게 됐다. 뉴욕은 민경 씨와 처음 만난 곳이라서 더욱 추억할 거리가 많았다.
오랜만에 하는 여행이라서 기대하는 마음보다도 얼떨떨함에 걱정이 조금 앞서기도 했었다. 둘 다 체력도 조금 유리인데다 잘 체하기도 해서 꽉 체운 일정을 우리가 잘 지킬 수 있을까 했는데 잘 짜인 계획 덕분에 유연한 대처도 가능했던 것 같다.
생각보다 사진을 많이 못찍었는데, 다음 스마트폰으로 교체하고 나면 더이상 카메라를 들고 다니지 않을 것 같기도 하다. 카메라 즐겁긴 하지만 점점 번거롭게 느껴지는게 아쉽다. 예전에 비해 또 사진에 대한 생각도 많이 달라지기도 했고.
계획할 때는 이게 마지막 뉴욕 여행이 될 것이란 얘길 하면서 일정을 짰는데 여행 후에는 우리 결국 못 본 곳들 많아서 적어도 다시 한 번은 와야겠다는 얘기를 했다.
큰 기대 없던 곳까지도 커피가 맛있다니, 집에 와서도 뉴욕서 마신 커피 얘기한다.
40+20 작업법은 어떻게 하는 건가요?
- 하루에 몇 KMN을 하겠다고 정한다(예: 8KMN)
- 쪽지에 그 횟수만큼 숫자를 쓴다(예: ➀➁➂➃➄➅➆➇).
- 몇 시든 좋으니 정각에 자리에 앉는다(예: 오전 10시).
- 40분 후 알려주도록 설정된 타이머를 켠다.
- 40분간 집중해서 작업한다.
- 타이머가 울리면 무조건 일어난 뒤, 1KMN을 했다고 표시한다(예: ➊➁➂➃➄➅➆➇).
- 20분 쉬면서 다른 일을 한다.
- 다시 정각이 되면(예: 오전 11시) 무조건 자리에 앉는다.
- 4)~8)을 목표 횟수만큼 반복한다(예: ➊➋➌➍➎➏➐➑).
- 하루 일을 마감한다(예: 오후 6시).
40+20 작업법에서 기억할 점
- 일할 때 집중합니다.
- 쉴 때 긴장을 풉니다.
- 일할 때 다른 문제를 걱정하지 마세요.
- 쉴 때 일을 걱정하지 마세요.
- 가급적 정각에 시작하세요.
- 앱에 의존하지 마세요.
- 하루에 10KMN 이상 하지 마세요.
-- 40+20 작업법
간단하게 현재 상황과 남은 일을 확인할 수 있는 시스템을 구축하는 접근 방식이 너무나도 마음에 든다. 외부 의존도를 낮추면서도 단순하게 작은 종이에 할 일을 적는 방법은 정말 해야 할 일에 힘을 집중할 수 있게 한다.
어쩌다보니 블로그 업데이트를 연례 행사처럼 치루고 있다.
그동안 Gatsby를 잘 사용하고 있었지만 걷어내기로 했다. netlify 인수 이후엔 업데이트 자체도 상당히 정체된 상황이다. 새 버전에 대한 이야기가 있긴 하지만 수많은 플러그인이 동시에 관리되고 업데이트 해왔던 그간의 방향성을 봤을 때 조금 의구심이 들 수 밖에 없는 상황이다. 거기에다 Gatsby Cloud도 접은 것 보면 사용자로서는 좀 김이 빠진다. 정말 좋아하는 프로젝트고 프로덕트였는데 프로덕트의 성숙도와는 달리 순식간에 불투명해져버린 상황이 많이 아쉽다.
react 기반의 정적 사이트 생성기를 계속 사용할까 찾아봤다. Astro가 요즘 유행이기도 하고 Vercel 제품도 사용해보고 싶긴 하지만... 1) 웹브라우저의 기본적인 사이클과 잘 맞지 않는 기분이 종종 들었고 (아무래도 웹사이트니까 SPA같은 느낌이 드는게 묘하게 어색한 그런 기분), 2) 마크다운에 마음대로 인라인 스크립트나 스타일을 넣는 일도 좀 불편한 기분이고, 3) 간단한 html을 넣으려고 이런 저런 컴포넌트를 오가야 하는 일도 조금 번거로웠다. 물론 react 문제 아니고 내가 구성한 방식의 문제겠지만서도.
다른 정적 생성기를 사용할까 찾아보다가 그냥 간단하게 만들기로 했다. 페이지네이션 없이 그냥 목록만 제공할 거니까, 복잡하게 생각하지 말고 그냥 생각 가는데로 간단하게 만들기로 했다.
그동안 remark.js를 gatsby 통해서 잘 사용해왔으니까 이젠 직접 사용하기만
하면 될 것 같았다. 이미 라우팅은 각각 마크다운 파일에 정의된 frontmatter
를
사용해서 만들어내고 있었기 때문에 frontmatter
만 읽어 처리할 수 있으면 되는
상황이었다. remark.js과 여러 플러그인 통해서 별 문제 없이 구현할 수 있었다. 사실
gatsby 대부분 마크다운과 관련된 플러그인은 remark.js의 플러그인을 랩핑한
것이라서, 좀 더 날 것의 형태로 사용할 수 있다는 것은 오히려 장점이었다.
템플릿도 별도 엔진을 사용할까 하다가도 새로운 템플릿 문법을 보고 짜는 것도 번거롭고 이미 리터럴을 잘 쓰고 있으니 간단하게 템플릿 리터럴로 구현했다. 페이지 컨텐츠는 라우팅과 분리했지만 템플릿은 라우팅을 기준으로 불러오게 해서 페이지마다 스타일을 바꾸기 쉽게 만들었다. 그 외에도 sitemap.xml, RSS 피드, 리다이렉션, 이미지 처리 등 필요한 요소도 추가했다.
아직 별도의 캐시를 넣은 것도 아닌데 2~3분 걸리던 빌드가 30초대로 내려왔다. 빌드 로그를 보면 빌드 자체는 금방인데 필드 환경을 불러오는 시간이 꽤 길었다. netlify에서 cloudflare pages로 변경하고 싶은데 cloudflare pages는 빌드 시간 제한이 아니라 횟수 제한이라서 사이트가 좀 더 정리되면 그때 옮길 생각이다.
블로그를 변경하면서 간단한 템플릿 엔진이 필요했다. 템플릿 엔진을 사용하려고
살펴보니 새 문법을 배우는 것도 번거롭고 정말 작은 부분 때문에 의존성을 추가하는
것도 별로 맘에 들지 않았다. 그동안 잘 사용해온 템플릿 리터럴을 그냥 사용할
방법은 없을까 찾아보다가 Function
생성자를 사용해서 다음처럼 작성할 수
있었다.
"use strict";
function template(html, params = {}) {
const keys = Object.keys(params);
const ps = keys.map(k => params[k]);
return new Function(
...keys,
'return (`' + html + '`)')
(...ps);
}
Function
생성자는 파라미터 목록과 함수 내에 들어갈 내용을 문자열로 받고 그
함수를 반환한다. 위에서 보면 전달한 params
에서 키 값을 수집해 함수를
생성하는 일에 사용하고 또 키 이름 순서에 맞게 ps
배열을 만들어 함수에
전달했다.
이제 html
로 문자열을 전달하면 템플릿 리터럴로 이용할 수 있다.
const welcomePage = '<h1>${name}님 환영합니다.</h1>'
template(welcomePage, {name: '거북이'})
// "<h1>거북이님 환영합니다.</h1>"
다만 "`" 문자 사용에 주의해야 한다.
const welcomePage = '<h1>\\`${name}\\`님 환영합니다.</h1>'
template(welcomePage, {name: '거북이'})
// "<h1>`거북이`님 환영합니다.</h1>"
부분함수도 손쉽게 만들 수 있다.
function partial(html) {
return function render(params) {
return template(html, params);
}
}
const accountPage = '<button>${name}님의 계정 정보</button>'
const accountPartial = partial(accountPage)
const me = accountPartial({name: '나'})
// "<button>나님의 계정 정보</button>"
const koala = accountPartial({name: '코알라'})
// "<button>코알라님의 계정 정보</button>"
템플릿 함수가 필요하다면 다음처럼 params
에 함께 전달할 수 있다.
const balance = () => 3000 // 또는 좀 더 복잡한 코드
const balanceInfo = template('<strong>잔고: ${balance()}</strong>')({balance})
nodejs에서 사용하고 있기 때문에 nodejs의 모듈을 사용해 html을 불러오는 함수를
다음처럼 작성했다. 또한 템플릿 내부에서 별도의 파일을 불러올 수 있도록 load
함수를 경로 맥락과 함께 템플릿 내로 전달했다.
import fs from 'fs'
import path from 'path'
function load(filename, basePath = '.') {
const dirname = path.dirname(filename);
const html = fs.readFileSync(path.join(basePath, filename));
const partialHtml = partial(html);
return function loadPartial(params) {
return partialHtml({
...params,
load: function loadFromSubDir(filename) {
return load(filename, dirname);
}
})
}
}
<!-- ./templates/index.html -->
<h1>안녕하세요!</h1>
${load('./partials/info.html')({user})}
<!-- ./templates/partials/info.html -->
<p>${user.name}님의 계정 정보</p>
load('./template/index.html')({user: {name: '당근'}})
// "<h1>안녕하세요!</h1>\n\n<p>당근님의 계정 정보</p>"
템플릿 내에서 다른 유틸리티 함수가 더 필요하다면 위 함수와 같이 더 추가하면 되겠다.
사용자 입력을 직접 템플릿에 사용하는 것은 당연히 위험하다! 상황에 맞게 적절한 예비 조치가 필요하다.
function sanitize(text) {
return text.replace(/[^ㄱ-ㅎ|가-힣|a-z|A-Z|0-9| ]+/gi, "")
}
template(
"<p>${sanitize(name)}님 안녕하세요!</p>", {
name: "<strong>헤헤</strong>",
sanitize,
})
// "<p>strong헤헤strong님 안녕하세요!</p>"
이렇게 작은 템플릿 함수를 작성해봤다. 템플릿 리터럴을 활용하면 몇 줄 안되는
코드로도 템플릿 함수를 구현할 수 있었다. 간단한 수준에서라면 이 함수로도 충분히
활용 가능하겠지만 이 접근 방식의 한계(Function
내에서 바깥 스코프에 접근할 수
있는 등)로 보안 문제가 발생할 수 있다. 이런게 가능하다 정도로만 이해하고 제대로
된 라이브러리를 활용하는 것이 더 바람직하다.
가능한 한 자주 글을 써라. 그게 출판될 거라는 생각으로가 아니라, 악기 연주를 배운다는 생각으로.
Write as often as possible, not with the idea at once of getting into print, but as if you were learning an instrument.
-- J.B. priestley
Google Chrome을 주로 사용하고 있는데 언제부터인지 브라우저 내 PDF 뷰어가 엄청 느려졌다. Mozilla의 pdf.js가 Chromium 확장을 제공하고 있어서 설치해봤는데 만족스럽다. Manifest V2로 작성된 확장이라서 크롬 웹 스토어에 있는 버전은 더이상 관리되지 않는 것 같다. 그래도 직접 빌드를 한 경우엔 아직까지도 문제 없이 설치할 수 있다.
$ git clone https://github.com/mozilla/pdf.js.git && cd pdf.js
$ npm install --global glup-cli
$ npm install
$ glup chromium
Google Chrome에서 chrome://extensions
를 연 후 좌측 상단에 Load unpacked 버튼을 클릭, 그리고 pdf.js
폴더 내에 build/chromium
을 선택하면 확장을 설치할 수 있다. Firefox에 내장되어 있는 pdf 뷰어와 동일하게 동작한다.