3월 4주차 주간회고
눈코뜰 새 없이 바빴던 3월의 끝에 서서 한 주를 되돌아본다.
이 글은 3월 4주차 회고라고 쓰고 참회록이라 읽는 것이 좀 더 맞을 것 같다.
배에 탑승하다
항해 플러스를 시작했다.
5년차, 6년차씩이나 되는 놈이 왜 그런 부트캠프 같은걸 하냐고 생각할 수도 있지만 2개월 또는 3개월 길게는 6개월.. 아무튼 나는 그러한 부트캠프가 쌩초보나 신입보다는 코드 좀 만져봤다 근데 조금 애매한 연차거나 실력이나 싶은 사람들이 하면 더 효과적이라고 생각한다.
어쩌면 그냥 내 실력이 애매하거나 부족하다는 것을 포장하기 위한 생각일 수도 있겠다.
어쩌면 나는 연차에 비해 부족한 실력을 갖고 있는게 진실일 수도 있다.
전 회사를 다니면서 3년차 정도를 지나친 시점부터 나는 나의 실력에 의문부호를 내내 품고 있었다. 그리고 지금 이 순간까지도 개발자로서의 나의 에고는 그런 물음표의 심연을 헤매고 있는 듯 하다. 어쩌면 내 성격 상 모든 것을 내려놓기 전까지는 계속 그런 강박 같은 것에 시달릴 것이다.
올해 초였나, 쇼츠 스크롤을 내리다가 한 영상을 보았다. 그 영상에 따르면 세상엔 네 종류의 사람이 있다.
- 목표가 분명하며 노력하는 사람
- 목표는 없지만 노력하는 사람
- 목표는 있지만 노력하지 않는 사람
- 목표도 없고 노력도 하지 않는 사람
나는 몇 번 유형일까, 따져보면 나는 욕심이 없거나 욕망의 크기가 작은 인간은 아니다. 권력욕, 출세욕심, 아주 부자가 되고싶다 이런 것보다는 내가 하는 일을 좋아하기 때문에 그래서 좀 더 잘하고 싶고 내가 만드는 것들이 좀 더 세상을 좋은 방향으로 바꿨으면 하는 그런 추상적인 내재적 동기가 존재한다.
나의 목표는 매우 불투명하고 구체적이지 않을지언정 좀 더 나은 개발자가 되고싶다는 한 줄의 욕심은 늘 가슴 속에 품고 있었다. (사직서같네,,,).
애석하게도 충분한 동기가 있음에도 나는 너무 게으른 사람이었다. 곰곰이 생각해보고 과감히 스스로를 3번 유형이라고 결론지었다. 영상에서 3번 인물은 열등감에 사로 잡혀 결국 노력을 폄하하며 세상은 전부 다 재능과 타고나는 것에 의해 바뀐다고 합리화하게 된다하더라. 아휴 추해라...
짧은 커리어동안 스터디에 참여한 적도 있고 개발 서적도 읽고 개인적인 공부도 했지만 그것들은 결국 흐지부지 끝나기 마련이었고 기록도 변변치 않아 배운 것들을 소화했다고 볼 수 없었다. 내가 한 공부들은 아주 손쉽게 휘발되어 응가마냥 배설되기 바빴고 나 그거 공부해본 적 있어 라는 말로 나 스스로를 치장하는 용도로 쓰일 뿐이었다.
엄밀히 따지면 노력이 없던 것은 아니지만 원하는 수준에 비해서는 부족했고 세상엔 무수히 많은 사람들이 나보다 더 많이 노력하며 살고 있다. 합리화는 쉬운데 그렇게 가벼운 맘으로 영상을 내리고 변화 없이 삶을 이어갈 것인가? 아니 나는 그러고 싶지 않았다.
그게 내가 배를 탄 이유다.
(무엇보다도 지인 중에 항해를 경험하고 좋은 평가를 내려준 사람들이 있었으며 작은 금액을 쓰는 것이 아니라 강제성이 부여되며 한참 욕심이 있을 시기의 사람들에게서 열정이나 동기 같은 것들을 얻고 스스로 채찍질을 하고 싶은 마음도 있다.)
Back To The Basic
뭘 해야할지 모르겠다면 처음부터 다시 시작하는 게 맞다고 생각한다. 근데 기초부터 닦는 답시고 아는 걸 공부하는 것은 매우 따분하고 지루한 일이다. 결국 나는 이미 알고 있는 것이란 오류에 빠져서 시작부터 열심히 하지 않거나 대충할 가능성이 높다.
그런 의미에서 항해에서의 첫 주차는 매우 인상적이고 흥미로웠다. 첫 주차 과제는 Vanilla JS로 SPA를 만드는 일이었는데 절대적으로 난이도가 높다고 볼 수는 없었다. 아참 여기서 난이도에 대한 나만의 정의를 하자면
- 문제를 풀어갈 충분한 제반 지식이 있지만 겪어본 적 없거나 생소해서 어려움을 겪는 것
- 문제를 풀어갈 지식과 경험이 절대적으로 부족해서 어려움을 겪는 것
이렇게 두 가지 유형이 있을 것인데 이번 과제는 1번에 좀 더 가깝다는 생각을 했다. 일단 타입이 없는 쌩 자바스크립트를 다루는 일은 정말 오랜만이고 라우터나 스토어를 직접 구현하는 것은 처음이다.
개인적으로 React나 Vue의 내부 동작과 철학을 더 잘 이해하고 싶어서 나만의 React를 만드는 것을 시도한 적이 있었는데 그 역시 이런저런 이유로 찔끔찔끔 하다가 끝나버린 기억이 있다. 그래서 이런 과제를 접하는 것이 반가우면서도 한편으로는 내가 진작에 이런 걸 스스로 해봤다면 항해를 하지 않았을까 나 스스로에게 만족하며 살고 있을까? 잡생각이 들었다.(N이 맞구나 나,,,)
아무튼 나는 내가 가진 모든 지식과 경험을 동원하여 과제를 시작했다.
난 무엇을 어떻게 했고 뭘 배웠나
라우터와 스토어
과제에서 요구한 바를 이행하려면 라우팅 시스템과 스토어, 컴포넌트 아키텍쳐 등을 설계해야했다. 일단 나는 직관에 의존해서 설계를 짧게 시작했고 이내 코드를 작성했다.
class Store {
constructor() {
this.state = {
user: null,
isLoggedIn: false,
}
this.listeners = []
}
getState() {
return this.state
}
setState(newState) {
this.state = { ...this.state, ...newState }
this.listeners.forEach((listener) => listener(this.state))
}
subscribe(listener) {
this.listeners.push(listener)
return () => {
this.listeners = this.listeners.filter((item) => item !== listener)
}
}
}
const store = new Store()
export default store
import store from './store'
export default class Router {
root = null
routes = {}
authRequired = ['/profile']
publicOnly = ['/login']
constructor(routes) {
this.routes = routes
this.root = document.getElementById('root') || document.body
window.addEventListener('DOMContentLoaded', () => this.handleRoute())
this.initEventListeners()
this.initClickHandler()
}
initEventListeners() {}
initClickHandler() {
// 대충 앵커 태그 클릭했을 때의 처리를 글로벌로 선언
}
handleClickLink() {}
getPath() {}
checkAccess(path) {
// 대충 인증 상태 기반으로 페이지 접근 가부를 체크하는 함수
}
handleRoute() {
// checkAccess 후에 컴포넌트를 렌더링하는 함수
}
navigateTo() {}
}
최근 회사에서 클래스 코드를 볼 일이 많아서 그런지 클래스로 빠르게 작성해버렸다. 그러나 생각해보면 스토어나 라우터는 단일 프로젝트에서 단 하나뿐인데 굳이 클래스로 만들 이유가 있을까?
뭘로 만들든 만드는 놈 자유지만 좀 더 적정 기술이란 것은 존재하기 마련이다. 사실 요정도 되는 규모의 과제엔 뭘 쓰든 상관은 없다만 그런 고민을 한 건 나뿐만이 아니긴 하더라. 이런 고민에 대한 해답을 멘토님이 주셨다.
하나의 철학과 패러다임으로 운영되는 기술이나 오픈소스는 없으니 무엇을 쓰든 상관 없다. 필요에 맞게 본인이 원하는 철학에 맞게 적절히 구성하면 될 일이다.
라우터는 히스토리 라우터와 해쉬 라우터를 둘 다 만들어야해서 클래스 기반으로 만들어서 메소드 상속과 오버라이딩을 하면 보기도 편하고 개선도 쉽겠다는 생각이 분명했는데 스토어는 딱히 그런 생각이 들지 않긴 했다. React를 처음 배우던 시절을 생각하며 내 머리 속에 남아있던 Redux 코드들을 생각하며 그것과 비슷하게 코드를 고쳤다. 뭐가 더 맞는진 모르겠지만 내 눈엔 이게 더 편하고 잘 쓸 것 같다는 생각이다.
컴포넌트
컴포넌트는 순수히 UI만 렌더링 하는 게 있고 상태와 로직을 갖는 것들이 있다.
const Footer = () => `
<footer class="bg-gray-200 p-4 text-center">
<p>© ${new Date().getFullYear()} 항해플러스. All rights reserved.</p>
</footer>
`
export default Footer
const Header = ({ isLoggedIn }) => {
const isActive = (path) => window.location.pathname === path
const navigationLinks = isLoggedIn ? [] : []
const template = `
<header class="bg-blue-600 text-white p-4 sticky top-0">
<h1 class="text-2xl font-bold">항해플러스</h1>
</header>
<nav class="bg-white shadow-md p-2 sticky top-14">...</nav>
`
const init = () => {
const logoutBtn = document.getElementById('logout')
if (logoutBtn) {
logoutBtn.addEventListener('click', logout)
}
}
return {
template,
init,
}
}
export default Header
Footer는 순수 UI 컴포넌트고, Header는 상태와 로직을 갖는다. 어렵게 생각할 것 없이 React 컴포넌트의 모습과 유사하게(?) 작성을 했다.
문제는 여기서부터 다. 컴포넌트의 성격이 다를 순 있지만 인터페이스는 동일해야한다고 생각했다. 그런 의미에서 나는 컴포넌트를 만드는 클래스와 같은 틀이 필요하다고 느꼈고 현재 Header 와 같이 템플릿과 로직을 처리하는 부분으로 간단히 나눈 createComponent
를 만들었다.
export function createComponent(render, init) {
return (props = {}) => {
return {
html: render(props),
mount: (container) => {
if (init) {
init(container, props)
}
},
}
}
}
import { createComponent } from '../core/component.js'
const Header = createComponent(
({ isLoggedIn }) => {
const isActive = path ? '' : ''
const navigationLinks = isLoggedIn ? [] : []
return `
<header class="bg-blue-600 text-white p-4 sticky top-0">
<h1 class="text-2xl font-bold">항해플러스</h1>
</header>
<nav class="bg-white shadow-md p-2 sticky top-14">...</nav>
`
},
(container, { isLoggedIn }) => {
const logoutBtn = container.querySelector('#logout')
if (logoutBtn && isLoggedIn) {
logoutBtn.addEventListener('click', logout)
}
}
)
export default Header
컴포넌트 코드의 인터페이스가 통일되었고 템플릿과 로직이 어떻게 처리되는지가 미약하게나마 추상화되었다. 컴포넌트를 크게 상태와 그것들을 다루는 로직으로 나눌 수 있지만 그들이 전부는 아니다. 그런 의미에서 매우 빈약한 수준의 기능을 가진 컴포넌트 인터페이스라고 볼 수 있다.
컴포넌트에는 라이프싸이클도 있으며 상태와 props도 존재하고 메타데이터들도 필요하다. 그래서 그것들을 일부 추가하여 개선된 인터페이스를 만들었다.
export function createComponent(options = {}) {
const {
name,
render,
onMount,
onUpdate,
onUnmount,
defaultProps,
defaultState,
} = options;
return (props = {}) => {
... 생략 ...
return {
html: render({ ...currentProps, ...currentState, children }),
mount: (target) => { onMount(); },
update: (newProps) => { onUpdate() },
unmount: () => {
// 자식부터 unmount
// state 초기화
},
};
};
}
물론 이것도 미약하지만 기본적인 라이프싸이클과 상태, props와 메타데이터(name 꼴랑 하나ㅋ)를 가진 인터페이스가 탄생했다. 이 인터페이스로 컴포넌트를 다 통일하고 나서야 그나마 제 구실하는 컴포넌트 만들었구나,,, 싶은 생각이 들었다.
이 외에도 라우터에서 AuthGuard를 따로 빼서 체이닝 방식으로 적용한 것이라던가 로그인, 로그아웃 따위의 인증과 관련된 로직들을 서비스로 묶어서 가벼운 추상화를 시도하는 등의 작업이 있었다.
회사에서 기존에 있던 코드들을 만지는 것이 아닌 나의 입맛에 맞게 홀로 작은 시스템을 설계하고 만드는 일이라 즐거웠고 내가 여기저기서 주워듣고 겪고 배운 것들을 녹여낼 수 있고 좋은 구조란 무엇인가? 내가 가진 선호하는 철학이나 시스템의 모습은 어떤 것인가 고민거리를 잔뜩 던져준 과제였다.
시간적인 압박도 있고 어디까지나 과제이기 때문에 이번 주차 이후로 앞으로 최소 10주간 다시 만질 일은 없겠지만 분명한 것은 개인적으로 내가 작성한 코드가 그다지 만족스럽지 않았고 좀 더 나은 구조 설계를 위해 다른 사람들의 코드도 많이 보고 복습도 필요하다고 느꼈다.
아 그리고 예전에 실패로 끝나버린 나만의 React를 만들어보겠다는 그런 생각에 다시 불이 붙었다. 항해가 끝나면 다시 그것을 시작해볼 생각이다.