2주차 과제도 <프레임워크 없이 SPA 만들기>로, 주제는 이벤트 관리와 렌더링 성능 최적화다.
목표는
이다.
가상돔은 DOM을 객체로 변환한 것이다.
완전히 똑같이 객체로 만든 건 아니고 DOM 조작에 필요한 속성만 골라서 만든 가벼운 복사본이다.
가상돔을 쓰는 이유를 검색해보면 대부분 '효율적인 렌더링을 위해서'라고 나온다.
가상돔이 어떻게 렌더링을 효율적으로 해주는 걸까?
브라우저의 렌더링 과정을 간략하게 살펴보면 위와 같다.
이 과정에서 리렌더링이 가장 많은 비용이 발생한다고 한다.
SPA는 하나의 HTML 파일에서 JavaScript로 DOM API를 사용해서 화면을 조작한다.
애플리케이션의 규모가 커질 수록 조작할 일도 많아질 것이고, 그럴 수록 브라우저에게 더 많은 부하가 걸릴 것이다.
브라우저의 부하를 줄이고자 diff 알고리즘을 사용하여 기존 가상돔과 새로운 가상돔을 비교하여 변경된 부분만 업데이트한다.
여기까지 찾아보니 의문이 들었다.
실제 렌더링을 효율적으로 하는 건 diff 알고리즘을 이용하는 거면 가상돔은 왜 쓰는 걸까?
diff 알고리즘을 써서 변경할 부분을 찾고, 그 부분만 DOM API로 업데이트 해주면 렌더링 효율도 좋아지고 가상돔을 쓸 필요가 없는 거 아닐까?
실제로 성능면으로만 봤을 때는 바닐라 자바스크립트가 리액트보다 뛰어나다고 한다.
리액트로 프로젝트 경험이 없는 나의 의견이지만, 리액트를 써서 얻는 가장 큰 이점은 선언형 프로그래밍을 할 수 있다는 것이다.
탭을 클릭하면 선택한 탭은 활성화된 스타일을 넣고, 나머지 탭은 비활성화된 스타일을 넣어줘야 한다고 해보자.
<div id="parent">
<button class="tab">Tab 1</button>
<button class="tab">Tab 2</button>
<button class="tab">Tab 3</button>
</div>
<script>
const tabClickHandler = (e) => {
if(e.target.classList.contains("tab")){
document.querySelectorAll(".tab").forEach(tab => {
tab.classList.remove("active");
});
e.target.classList.add("active");
}
};
document.querySelector("#parent").addEventListener("click", tabClickHandler);
</script>
바닐라 자바스크립트는 명령형 프로그래밍으로 모든 과정을 코드로 작성해줘야 한다.
DOM을 선택하여 이벤트 리스너를 할당하고, tab
클래스를 모두 순회하며 active
클래스를 삭제해주고, 클릭한 대상에 active
클래스를 넣어준다.
이보다 더 많은 동작을 한다면 그 동작의 과정까지 모두 작성해야 한다.
function Tabs() {
const [activeTab, setActiveTab] = useState(0);
return (
<div id="parent">
{tabs.map((tab, index) => (
<button
key={index}
className={`tab ${activeTab === index ? 'active' : ''}`}
onClick={() => setActiveTab(index)}
>
{tab.label}
</button>
))}
</div>
);
}
반면 리액트는 선언형 프로그래밍으로 탭 클릭 시 activeTab
의 상태만 바꿔주면 activeTab === index
조건을 검사하여 알아서 탭의 스타일을 바꿔준다.
또한, 버튼을 Tab
이라는 컴포넌트로 만든다면 재사용성 재사용성이 높아진다.
어느 화면에서든 탭이 필요하다면 컴포넌트를 불러와 사용하면 된다.
선언형 프로그래밍의 장점은 화면을 어떻게 업데이트할지보단 무엇을 보여줄지에 집중할 수 있다.
반면 단점은 과정을 추적할 수 없다는 것이다.
이런 단점이 있으니, 과제를 구현하면서 내부 과정을 알게 하는 게 과제의 주된 목적이 아닐까 생각했다.
얘기가 옆으로 샜다.
아무튼 가상돔은 선언형 프로그래밍을 할 수 있게 해주는 걸까?
음... 사실 잘 모르겠다.
검색해보면 추상화를 해서... 어쩌구... 저쩌구.. 하는데 피부로 와닿지 않는다.
어떻게 선언형 프로그래밍으로 바꿔주는 걸지 고민해보면서 진행해봐야겠다.
jsx는 자바스크립트와 마크업 문법을 함께 사용할 수 있게 해주는 문법이다.
<div id="app">
<ul>
<li>
<input type="checkbox" class="toggle" />
todo list item 1
<button class="remove">삭제</button>
</li>
<li class="completed">
<input type="checkbox" class="toggle" checked />
todo list item 2
<button class="remove">삭제</button>
</li>
</ul>
<form>
<input type="text" />
<button type="submit">추가</button>
</form>
</div>
위와 같은 HTML을 가상돔 객체로 만들기 위해서는 변환이 필요하다.
바벨 홈페이지에 접속하여 "Try it out" 메뉴에 가보면 실제로 어떻게 변환되는지 나와있다.
기존에는 _jsxs
가 아니라 React.createElement
로 나왔던 걸로 아는데 버전이 바뀌면서 이름이 바꼈다.
const app = (
<div id="app">
<ul>
<li>
<input type="checkbox" class="toggle" />
todo list item 1<button class="remove">삭제</button>
</li>
<li class="completed">
<input type="checkbox" class="toggle" checked />
todo list item 2<button class="remove">삭제</button>
</li>
</ul>
<form>
<input type="text" />
<button type="submit">추가</button>
</form>
</div>
);
실제로 아무런 설정없이 위와 같이 jsx 문법을 사용하면 오류가 발생한다.
@babel/core
와 @babel/plugin-transform-react-jsx
를 추가해줘야 하는 걸로 알고 있었다.
하지만 팀원분게서 vite
환경에서는 별도의 설치없이 된다고 알려주셨다.
파일 상단에 /* @jsx */
를 추가해주면 기본값으로 /* @jsx React.createElement */
가 들어간다.
이 React.createElement
를 /* @jsx 함수이름 */
처럼 직접 작성한 함수의 이름으로 변경하면 가상돔 객체를 함수에서 인자로 받을 수 있다.
과제에서는 createVNode
로 작성되어 있고 파일들이 분리되어 있었다.
이번에도 테스트 코드만 보면서 과제는 통과시켜놨는데 사실 내부 로직이 왜이렇게 되는지는 알지 못해서 하나씩 뜯어보며 리팩토링 해보자.
과제에서 구현된 로직은 다음과 같다.
renderElement(<Page />, $root);
로 페이지가 $root
에 렌더링된다.<Page />
는 Page()
와 같다. 함수형 컴포넌트를 실행시킨 것이다. 페이지 컴포넌트는 jsx를 반환할테니 createVNode
의 결과값이 renderElement
의 첫 번째 인자로 넘어간다.renderElement
에서 createVNode
가 반환한 가상돔을 normalizeVNode
에서 표준화한다.normalizeVNode
가 반환한 최종적인 가상돔을 createElement
을 통해 실제 DOM으로 변환한다.updateElement
에서 diff 알고리즘을 활용하여 필요한 부분만 변경한다.가장 먼저 실행되는 createVNode
를 뜯어보자.
export function createVNode(type, props, ...children) {
children = children
.flat(Infinity)
.filter(
(child) =>
child !== null && typeof child !== "undefined" && child !== false,
);
return { type, props, children };
}
최종 구현된 createVNode
다.
왜 flat(Infinity)
를 활용해서 평탄화해야 하고, null
, undefined
, false
값을 걸러줘야 할까?
먼저 인자로 들어오는 값들을 확인해보자.
export const HomePage = () => {
const { currentUser, posts, loggedIn } = globalStore.getState();
return (
<div className="bg-gray-100 min-h-screen flex justify-center">
<div className="max-w-md w-full">
<Header />
<Navigation />
<main className="p-4">
{loggedIn && <PostForm />}
<div id="posts-container" className="space-y-4">
{[...posts]
.sort((a, b) => b.time - a.time)
.map((props) => {
return (
<Post
{...props}
activationLike={props.likeUsers.includes(
currentUser?.username,
)}
/>
);
})}
</div>
</main>
<Footer />
</div>
</div>
);
};
렌더링될 페이지다.
createVNode
로 이 페이지의 반환값이 인자로 들어올 것이다.
콘솔로 찍어보면
예상과는 다르다.
HomePage
자체가 넘어온다 왜일까?
이유는 renderElement(<Page />, $root);
과 같이 호출하기 때문이다.
바벨 홈페이지에서 확인해보면 <Page/>
자체도 _jsx
함수의 인자로 들어간다.
createVNode
의 최종 형태에서는 함수를 처리하는 부분이 없으므로 normalizeVNode
도 살펴보자.
export function normalizeVNode(vNode) {
if (
vNode === null ||
typeof vNode === "undefined" ||
typeof vNode === "boolean"
) {
return "";
}
if (typeof vNode === "string" || typeof vNode === "number") {
return vNode.toString();
}
if (typeof vNode.type === "function") {
const { type, props, children } = vNode;
vNode = normalizeVNode(type({ ...props, children }));
}
vNode.children = [...vNode.children]
.map(normalizeVNode)
.filter((child) => !!child);
return vNode;
}
코드를 보면 함수일 경우 실행하도록 되어있다.
두 함수를 따로 보기 힘드니 하나로 합치고 함수일 때 처리하는 부분만 남기고 렌더링할 페이지를 단순하게 바꿔보자.
export function createVNode(type, props, children) {
if (typeof type === "function") {
return type();
} else {
const element = document.createElement(type);
element.append(children);
return element;
}
}
export const HomePage = () => {
const { posts } = globalStore.getState();
return <h1>Hello World!</h1>;
};
합치는 김에 아예 createElement
의 기능도 몰았다.
간단한 HTML을 렌더링하는데 성공했다.
이제 속성도 넣고 자식 태그도 넣어서 점점 더 복잡하게 만들어보자.
export const HomePage = () => {
const { posts } = globalStore.getState();
return (
<div>
<h1 id="title" className="font-bold">
Hello World!
</h1>
<ul>
<li className="text-red-500">1</li>
<li className="text-green-500">2</li>
<li className="text-blue-500">3</li>
</ul>
</div>
);
};
export function createVNode(type, props, ...children) {
if (typeof type === "function") {
return type();
} else {
const element = document.createElement(type);
if (props) {
Object.entries(props).map(([key, value]) => {
if (key === "className") {
element.classList = value;
}
if (key.startsWith("on")) {
const eventType = key.slice(2).toLowerCase();
addEvent(element, eventType, value);
return;
}
element.setAttribute(key, value);
});
}
if (children) {
[...children].forEach((child) => {
element.append(child);
});
}
return element;
}
}
위 코드를 실행하면 정상동작한다.
처음에는 'element
를 쌓는 부분이 없는데 어떻게 정상 동작하는 걸까?'라고 생각했다.
이유는 createVNode
는 가상돔 전체를 쌓아서 반환하지 않는다.
실행 결과가 다음 createVNode
의 인자로 들어가는 걸 반복해서 최종적으로 실행된 createVNode
가 반환한 element
가 반환되는 것이다.
이해를 위해서 바벨에서 실행해보자.
HomePage
가 반환하는 내용이다.
순서를 살펴보면
div
태그가 생성되기 위해 children
을 인자로 넘겨줘야 하는데, children
에서도 createVNode
의 실행 결과를 넘겨줘야 한다.h1
태그가 먼저 생성되고 children[0]
로 들어간다.ul
태그가 생성되기 위해 li
태그가 먼저 만들어진다.li
태그가 각각 실행된 결과를 배열로 ul
태그에 넘겨준다.ul
태그에서 children
을 순회하며 자식으로 넣어준다.div
태그에서 children
을 자식으로 받은 결과를 반환한다.형제 관계라면 순서대로, 부모-자식 관계라면 자식 -> 부모 순서대로 실행된다.
다시 HomePage
내용을 되돌려서 처리해보자.
export const HomePage = () => {
const { currentUser, posts, loggedIn } = globalStore.getState();
return (
<div className="bg-gray-100 min-h-screen flex justify-center">
<div className="max-w-md w-full">
<Header />
<Navigation />
<main className="p-4">
{loggedIn && <PostForm />}
<div id="posts-container" className="space-y-4">
{[...posts]
.sort((a, b) => b.time - a.time)
.map((props) => {
return (
<Post
{...props}
activationLike={props.likeUsers.includes(
currentUser?.username,
)}
/>
);
})}
</div>
</main>
<Footer />
</div>
</div>
);
};
위 페이지가 정상적으로 나오게 하기 위해서 두 가지 작업을 해줘야 한다.
children
평탄화하고 null
, undefined
, false
값을 걸러줘야 한다((드디어 이유를 찾았다!).type
실행 시 {...props, children}
으로 넘겨주기먼저 children
을 평탄화하는 이유는
<div id="posts-container" className="space-y-4">
{
[...posts]
.sort((a, b) => b.time - a.time)
.map((props) => {
return (
<Post
{...props}
activationLike={props.likeUsers.includes(currentUser?.username)}
/>
);
});
}
</div>
위와 같은 상황때문이다.
createVNode
에서 ...children
으로 children
을 배열로 이미 받고 있는데 children
에 [...posts]
라는 배열이 넘어간다.
따라서 createVNode
에서 중첩 배열로 받게 되는데 이걸 그대로 로직을 태우면
화면에 element
배열이 그대로 출력되게 된다.
undefined
, null
, false
값을 걸러줘야 하는 이유도 화면에 문자열로 출력되기 때문이다.
type
실행 시 {...props, children}
으로 넘겨줘야 하는 이유는 과제를 제출하고 같은 팀원분께서 여쭤봤는데 제대로 대답하지 못했다.
테스트 코드를 통과하려다보니 이리저리 만지다가 통과가 됐기 때문인데, 코드를 뜯어보면서 이유를 찾게 됐다.
이유는 컴포넌트를 실행할 때 알맞게 인자로 넘겨주기 위해서인데, Link
태그를 예시로 살펴보자.
function Link({ onClick, children, ...props }) {
const handleClick = (e) => {
e.preventDefault();
onClick?.();
router.get().push(e.target.href.replace(window.location.origin, ""));
};
return (
<a onClick={handleClick} {...props}>
{children}
</a>
);
}
위 태그는 createVNode
에서 인자로
{ type: "Link", props: { href: "/", className: "getNavItemClass 결과값"}, children: ["홈"]}
형태로 넘어온다.
Link
는 함수니 실행시켜야 하고 인자를 받고 있으니 넘겨줘야 하는데 Link
함수의 시그니처를 살펴보면
function Link({ onClick, children, ...props })
위와 같이 구조 분해 할당으로 받고 있다.
그렇기 때문에 props
를 펼쳐서 보내주거나,
props.children = children;
type(props);
로 해도 동일하게 동작할 것 같다.
이제는 vNode
의 타입이 number
일 경우 string
으로 반환해주기만 하면 화면에 렌더링할 수 있다.
과제에서는 normalizeVNode
나 createElement
에서 undefined
, null
등등 falsey
한 값들을 걸러주는 코드가 있는데, '이미 createVNode
에서 걸러줄 텐데 왜 필요한 걸까?'라는 고민이 있었다.
화면상에서는 체크가 되지 않아 과제 중간 Q&A때 코치님께 여쭤봤다.
코치님께서 함수가 단독으로 쓰일 수 있어 안정성을 위해 추가한 테스트 코드라고 답변해주셨다.
이로써 jsx의 반환값을 가상돔으로 변환하고 DOM으로 바꾸는 것까지 알아봤다.
명령형 프로그래밍에서 선언형 프로그래밍으로 동작할 수 있게 바뀌었다.
'꼭 가상돔이어야 선언형 프로그래밍을 구현할 수 있을까?'라는 의문에는 아닌 것 같다.
하지만 DOM은 많은 정보를 담고 있다.
DOM으로 비교하는 로직을 구현한다면 이 많은 속성들을 다 훑을 것이다.
'가상돔은 DOM에서 필요한 정보만 갖은 경량화된 돔 객체이기 때문에 비교 과정을 효율적으로 변경해준다.'가 과제를 하면서 내린 개인적인 결론.
이번에 새로 알게된 타입 WeakMap!
WeakMap의 최대 장점은 key로 설정한 값이 GarbageCollector(이하 GC)에게 정리된다면 key에 연결된 value도 정리된다!
주의할 점은 key
로 객체, 함수, 배열 등을 넣어줄 수 있다.
과제를 하면서 WeakMap을 활용할 수 있는 두 가지가 있었다.
let oldNode = null;
export function renderElement(vNode, container) {
vNode = normalizeVNode(vNode);
if (!container.childNodes[0]) {
oldNode = null;
}
// 최초 렌더링시에는 createElement로 DOM을 생성하고
if (!oldNode) {
container.append(createElement(vNode));
} else {
// 이후에는 updateElement로 기존 DOM을 업데이트한다.
updateElement(container, vNode, oldNode);
}
// 렌더링이 완료되면 container에 이벤트를 등록한다.
setupEventListeners(container);
oldNode = vNode;
}
oldNode
를 변수에 저장하고 renderElement
실행 시 container
가 비어있는지 체크한다.
이유는 테스트가 하나씩 실행될 때마다 body.removeChild
를 사용하여 내부를 삭제하고 다시 빈 div
태그를 생성하여 body
에 넣어주기 때문이다.
따라서 DOM 업데이트 시 실제 DOM은 비어있는데, 비교 과정에서 oldNode
는 요소가 있기 때문에 오류가 발생한다.
이를 해결하고자 'container
가 비어있으면 oldNode
에 null
을 넣어주자'라고만 생각했다.
슬랙에 같은 문제를 겪은 분께서 해결한 상황을 공유해주셨다.
다양한 방법을 고려하는 모습에 본받아야겠다고 생각했다.
사실 이때까지도 'WeakMap
이 뭐지? 나중에 한 번 알아봐야겠다!'라고 생각했다.
그러다 zep에서 팀원분들과 얘기를 하다가 이벤트 관리에대해서 얘기를 했는데, 한 분이 WeakMap
을 써서 관리하면 key
가 GC에 정리되면 value
도 정리된다는 걸 알려주셨다.
추가로 과제 중간 Q&A시간에도 코치님께서 한 번 더 말씀해주셔서 잘 써먹으면 용이한 타입이겠다라고 생각했다.
const eventHandlers = {};
export function setupEventListeners(root) {
Object.keys(eventHandlers).forEach((eventType) => {
root.addEventListener(eventType, handleEvents);
});
}
export function addEvent(element, eventType, handler) {
if (!eventHandlers[eventType]) {
eventHandlers[eventType] = new Map();
}
const elementHandlerMap = eventHandlers[eventType];
elementHandlerMap.set(element, handler);
}
export function removeEvent(element, eventType) {
if (eventHandlers[eventType] && eventHandlers[eventType].has(element)) {
eventHandlers[eventType].delete(element);
}
}
function handleEvents(e) {
const handlers = eventHandlers[e.type];
if (!handlers) return;
const handler = handlers.get(e.target);
if (!handler) return;
handler(e);
}
이벤트 관리하는 코드는 기존에 Map
을 사용해서 저장했다.
처음에는 CSS 선택자로 문자열 key
를 설정하려 했지만, 고유하지 않을 것 같다는 판단이 생겨서 element
를 통째로 저장했다.
아무튼 Map
으로 저장하면 문제(WeakMap을 알기 전까지 생각지도 못했지만)가 updateElement
시에 새로운 가상돔은 있지만 기존 가상돔이 없을 경우 DOM을 삭제하게 되는데 이벤트 리스너를 별도로 삭제하지 않았다.
기존 가상돔과 새로운 가상돔의 type
이 같아 속성을 변경할 경우에만 이벤트 리스너를 삭제해주고 있었는데, 페이지를 여러번 이동하면 Map
에는 이벤트 핸들러가 계속 쌓일 것이다.
하지만 WeakMap
을 사용하면 이러한 상황을 방지해준다.
key
로 갖고 있는 element
가 사라지면 WeakMap
에서 알아서 사라지기 때문이다.
const nodeToContainerMap = new WeakMap();
export function renderElement(vNode, container) {
vNode = normalizeVNode(vNode);
// 최초 렌더링시에는 createElement로 DOM을 생성하고
if (!nodeToContainerMap.has(container)) {
container.append(createElement(vNode));
} else {
// 이후에는 updateElement로 기존 DOM을 업데이트한다.
const oldNode = nodeToContainerMap.get(container);
updateElement(container, vNode, oldNode);
}
// 렌더링이 완료되면 container에 이벤트를 등록한다.
setupEventListeners(container);
nodeToContainerMap.set(container, vNode);
}
renderElement
또한 key
로 참조하고 있는 container
가 사라지면 value
가 같이 사라진다.
const eventHandlers = {};
export function setupEventListeners(root) {
Object.keys(eventHandlers).forEach((eventType) => {
root.addEventListener(eventType, handleEvents);
});
}
export function addEvent(element, eventType, handler) {
if (!eventHandlers[eventType]) {
eventHandlers[eventType] = new Map();
}
const elementHandlerMap = eventHandlers[eventType];
elementHandlerMap.set(element, handler);
}
export function removeEvent(element, eventType) {
if (eventHandlers[eventType] && eventHandlers[eventType].has(element)) {
eventHandlers[eventType].delete(element);
}
}
function handleEvents(e) {
const handlers = eventHandlers[e.type];
if (!handlers) return;
const handler = handlers.get(e.target);
if (!handler) return;
handler(e);
}
updateElement
는 diff 알고리즘을 사용하여 기존 가상돔과 새로운 가상돔의 달라진 부분을 찾고 달라진 부분의 DOM을 변경하여 효율적으로 렌더링하는 함수다.
사실 내용은 크게 어렵지 않은데, 구현하기가 어렵다.
내용만 살펴보면
이다.
구현하면서 가장 어려웠던 부분은 다른 점을 찾았는데, 어떻게 정확하게 해당하는 DOM을 선택할 수 있을까였다.
export function updateElement(parentElement, newNode, oldNode, index = 0) {
if (!newNode && oldNode) {
parentElement.removeChild(parentElement.childNodes[index]);
return;
}
if (newNode && !oldNode) {
const newElement = createElement(newNode);
parentElement.append(newElement);
return;
}
if (typeof newNode === "string" && typeof oldNode === "string") {
if (newNode != oldNode) {
parentElement.childNodes[index].textContent = newNode;
}
return;
}
if (newNode.type !== oldNode.type) {
const newElement = createElement(newNode);
const oldElement = parentElement.childNodes[index];
if (oldElement) {
parentElement.replaceChild(newElement, parentElement.childNodes[index]);
return;
}
parentElement.appendChild(newElement);
return;
}
const element = parentElement.childNodes[index];
const newChildren = newNode.children;
const oldChildren = oldNode.children;
const maxLength = Math.max(newChildren.length, oldChildren.length);
updateAttributes(element, newNode.props, oldNode.props);
for (let i = 0; i < maxLength; i++) {
updateElement(element, newChildren[i], oldChildren[i], i);
}
}
updateElement
에서 인자로 받은 index
와 반복문에서 할당한 i
가 코드에 섞여있어 애를 먹었다.
처음에는 '자식 요소를 파고 들면서 index
가 증가하나?'라고 생각했지만 열심히 디버깅해보니 아니였다.
음... 그림으로 표현한 거긴한데, 같은 색으로 표시한 것들이 하나의 반복문에서 돈다?
형제 요소가 3개가 있다면, 첫째가 0, 둘째가 1, 셋째가 2고 부모 요소에서 자식 요소로 넘어갈 때는 0이 된다.
따라서 parentElement
의 children
의 updateElement
로 받은 index
번지의 요소가 현재 검사하는 요소가 된다.
말로 설명하자니 좀 어렵긴 한데... 아무튼 그렇다.
중간에 조금 흐트러지긴 했지만, 10시에 일어나서 할 거 하고 책상에 앉아서 새벽 3시까지 있를 하고 있다.
쓸데없이 보내는 시간이 많지만 그래도 새벽 3시까지는 컴퓨터를 끄지 않고 있다.
절대적인 시간은 벌어놨으니, 시간을 알차게 쓸 고민을 해야겠다.
책상 앞에 앉아있는 시간은 늘었지만, 그 시간동안 공부를 하고 있나? 생각해보면 아닌 시간이 더 많다.
해야 할 일들을 몇가지 정해두고 시간 분배를 해야할까?
아무튼 시간을 어떻게 '잘' 보낼지 고민하고 개선해야 한다.
프로그래밍 책을 보면 보통 초반부에 이 기술이 나온 역사가 나온다.
사실 이 부분은 와닿지 않아서 대충 보고 넘겼는데, 멘토링 시간에 리액트가 어떤 문제를 해결하려고 나왔는지를 알면 왜 가상돔이란 전략을 썼고 다른 기술들은 왜 다른 방법을 차용한 건지에 대해서 알 수 있다고 했다.
같은 팀원 분께서 고맙게도 10시에 코테 연습을 같이 해주기로 하셨다.
일단 어떻게 할지 몰라서 프로그래머스의 기초 트레이닝을 하고 있다.
어떤 방법으로 하면 더 좋을지 하면서 개선해 나가야겠다.
기술을 공부하기 전에 어떤 문제가 있어서 나왔고 어떤 방식으로 해결하려 했는지를 알고 가는 게 전체적인 맥락을 이해하고 넘어가야겠다.