[항해 플러스 프론트엔드 4기] 1주차 과제 회고

원정·2024년 12월 22일
8
post-thumbnail

항해 플러스 프론트엔드 4기가 시작됐다.
첫 번째 과제는 <프레임워크 없이 SPA 만들기>였다.
총 3주에 걸쳐 진행되는 과제인데 첫 주는 라우팅, 컴포넌트 기반 구조 설계, 전역 상태 관리 구현을 했다.

💰 기술적 성장: 이벤트 위임 활용


항해가 시작되기 전 여러 사전 스터디 자료를 주셨다.
JavaScript 자료를 학습하며 이벤트 위임에 대해서 알게 됐다.
이벤트 위임을 간단하게 설명하면

<nav id="nav">
  <ul>
    <li><a href="/a" class="link">A</a></li>
    <li><a href="/b" class="link">B</a></li>
    <li><a href="/c" class="link">C</a></li>
  </ul>
</nav>

위 코드에서 a 태그에 이벤트 리스너를 추가할 때 기존에는

document.querySelectorAll("a.link").forEach((link) => {
  link.addEventListener("click", (e) => {
    e.preventDefault();
    console.log("click!");
  });
});

document.querySelectorAll로 모든 태그를 선택해 반복문을 통해 일일히 이벤트 리스너를 추가했다.

하지만 이벤트 위임을 사용하면

document.querySelector("#nav").addEventListener("click", (e) => {
  if (e.target.tagName === "A") {
    e.preventDefault();
    console.log("click!");
  }
});

상위 태그에 이벤트 리스너를 한 번만 할당한 뒤 이벤트 발생 target을 검사하여 원하는 로직을 수행할 수 있다.

이벤트 위임은 이벤트 버블링 덕분에 가능하다.
이벤트 버블링은 한 태그에서 이벤트가 발생하면 부모 방향으로 이벤트를 전파하는 현상을 말한다.
즉, 자식 요소에서 발생한 이벤트는 버블링을 통해 부모 요소에서 알 수 있다.
같은 이벤트 리스너를 추가하려는 태그들이 있다면, 공통 부모 태그에 이벤트 리스너 하나만 사용하여 이벤트 처리를 할 수 있다.

💰 과제하며 고민한 부분: 리팩토링


import MainPage from "./pages/MainPage";
import ProfilePage from "./pages/ProfilePage";
import LoginPage from "./pages/LoginPage";
import NotFoundPage from "./pages/NotFoundPage";
import UserStore from "./store/userStore";

const routes = {
  "/": () => MainPage(),
  "/profile": () => ProfilePage(),
  "/login": () => LoginPage(),
  404: () => NotFoundPage(),
};

const hashRoutes = {
  "#/": () => MainPage(),
  "#/profile": () => ProfilePage(),
  "#/login": () => LoginPage(),
  404: () => NotFoundPage(),
};

const router = createRouter();

function createRouter() {
  return function (path) {
    path = path ? path : window.location.pathname;
    let hash = window.location.hash;
    let route = null;
    const user = new UserStore().getUser();

    if (hash === "") {
      if (!user && path === "/profile") path = "/login";
      if (user && path === "/login") path = "/";
      route = routes[path] || routes["404"];
      window.history.pushState(null, "", path);
    } else {
      if (!user && hash === "#/profile") hash = "#/login";
      if (user && hash === "#/login") hash = "#/";
      route = hashRoutes[hash] || hashRoutes[404];
      window.history.pushState(null, "", hash);
    }

    initializeView(route);
  };
}

function initializeView(route) {
  const root = document.getElementById("root");
  render(route, root);
  attachEventListeners(root);
}

function render(route, root) {
  root.innerHTML = route();
}

function attachEventListeners(root) {
  const cloneRoot = root.cloneNode(true);
  cloneRoot.addEventListener("submit", submitEventHandler);
  cloneRoot.addEventListener("click", clickEventHandler);
  root.replaceWith(cloneRoot);
}

function submitEventHandler(e) {
  e.preventDefault();
  const form = e.target;
  const formData = new FormData(form);
  const { id } = form;

  if (id === "login-form") {
    const username = formData.get("username");

    if (username) {
      new UserStore().setUser({ username, email: "", bio: "" });
      router("/profile");
    }
  }

  if (id === "profile-form") {
    const username = formData.get("username");
    const email = formData.get("email");
    const bio = formData.get("bio");

    new UserStore().setUser({ username, email, bio });
    router("/profile");
  }
}

function clickEventHandler(e) {
  const { id, tagName } = e.target;

  if (tagName === "A") {
    e.preventDefault();
    const { href } = e.target;
    let path = href.slice(href.lastIndexOf("/"));
    if (id === "logout") {
      new UserStore().deleteUser();
      path = "/login";
    }
    router(path);
  }
}

window.addEventListener("popstate", () => router());
window.addEventListener("load", () => router());
window.addEventListener("hashchange", () => router());

현재 코드의 문제점은 아래와 같다.

  1. 라우팅, 이벤트 처리, 렌더링이 모두 한 파일에 있다.
  2. 라우터 로직들이 각각 따로 선언되어 있다.

라우터를 분리하기 전에 라우터에게 기대하는 역할은 무엇일까?

개인적으로 생각하는 라우터의 역할은 URL 경로에 따라 알맞는 페이지 컴포넌트를 찾아서 렌더링해주는 것이다.

현재 라우터는 어떤 역할을 수행하고 있는가?

  1. path를 인자로 받거나 없으면 window.location.pathname을 변수에 담는다.
  2. 현재의 hash도 변수에 담는다.
  3. 경로 기반 라우팅인지, 해시 기반 라우팅인지와 로그인 유무에 따라 페이지 컴포넌트를 가져온다.
  4. 브라우저의 history에 경로를 저장한다.
  5. root에 페이지를 렌더링하고 이벤트 리스너를 추가한다.

💵 관심사 분리: 이벤트 리스너 분리

이벤트 리스너를 추가하는 건 라우터의 역할이 아니므로 분리해보자.

function attachEventListeners(root) {
  const cloneRoot = root.cloneNode(true);
  cloneRoot.addEventListener("submit", submitEventHandler);
  cloneRoot.addEventListener("click", clickEventHandler);
  root.replaceWith(cloneRoot);
}

이벤트 위임을 사용해 root에 이벤트 리스너를 추가하고 있다.
root에 내용이 변할 때마다 이벤트 리스너를 새롭게 추가해주고 있는데 생각해보니 그럴 필요가 있나?

  1. root가 변하는게 아니라 자식 요소가 변하기 때문에 이벤트는 한 번만 할당하면 된다.
  2. body 태그에 root 태그만 있으니 코드 가독성을 위해 bodyclick 이벤트와 submit 이벤트 두 개만 추가해주면 router에서 이벤트 리스너를 추가하지 않아도 되고 코드도 줄어든다.
document.body.addEventListener("submit", submitEventHandler);
document.body.addEventListener("click", clickEventHandler);

위 두 줄을 추가하고 attachEventListener 함수와 호출하는 부분의 코드를 삭제했다.

💵 파일 분리: 라우터 로직 캡슐화

이벤트 처리를 분리했으니 라우터 관련 코드를 다른 파일로 분리시켜서 main.js에 필요한 것만 보내도록 변경해보자.

// src/router/createRouter.js
import MainPage from "@/pages/MainPage";
import ProfilePage from "@/pages/ProfilePage";
import LoginPage from "@/pages/LoginPage";
import NotFoundPage from "@/pages/NotFoundPage";
import UserStore from "@/store/userStore";

const routes = {
  "/": () => MainPage(),
  "/profile": () => ProfilePage(),
  "/login": () => LoginPage(),
  404: () => NotFoundPage(),
};

const hashRoutes = {
  "#/": () => MainPage(),
  "#/profile": () => ProfilePage(),
  "#/login": () => LoginPage(),
  404: () => NotFoundPage(),
};

export function createRouter() {
  return function (path) {
    path = path ? path : window.location.pathname;
    let hash = window.location.hash;
    let route = null;
    const user = new UserStore().getUser();

    if (hash === "") {
      if (!user && path === "/profile") path = "/login";
      if (user && path === "/login") path = "/";
      route = routes[path] || routes["404"];
      window.history.pushState(null, "", path);
    } else {
      if (!user && hash === "#/profile") hash = "#/login";
      if (user && hash === "#/login") hash = "#/";
      route = hashRoutes[hash] || hashRoutes[404];
      window.history.pushState(null, "", hash);
    }

    render(route);
  };
}

function render(route) {
  const root = document.getElementById("root");
  root.innerHTML = route();
}

부족한 점이 많지만 그 중 두 가지만 뽑자면

  1. createRouter에서 반환하는 router는 인수로 path를 받아서 사용하기도 하고 인수로 받지 않을 경우에는 window.location.pathname을 받아서 사용한다.
  2. router에서 일반 라우트와 해시 라우트 처리를 모두 하고 있다.

위 두 문제를

  1. path를 받아서 사용하는 navigator 함수를 만들어서 별도의 로직으로 분리.
  2. routerhashRouter 분리.

로 해결해보자.

import MainPage from "@/pages/MainPage";
import ProfilePage from "@/pages/ProfilePage";
import LoginPage from "@/pages/LoginPage";
import NotFoundPage from "@/pages/NotFoundPage";
import UserStore from "@/store/userStore";

export function createRouter() {
  const ROUTES = {
    "/": () => MainPage(),
    "/profile": () => ProfilePage(),
    "/login": () => LoginPage(),
    404: () => NotFoundPage(),
  };

  const HASH_ROUTES = {
    "#/": () => MainPage(),
    "#/profile": () => ProfilePage(),
    "#/login": () => LoginPage(),
    404: () => NotFoundPage(),
  };

  function render(route) {
    const root = document.getElementById("root");
    root.innerHTML = route();
  }

  function validateRouteUser(path) {
    const user = new UserStore().getUser();
    if (!user && path === "/profile") path = "/login";
    if (user && path === "/login") path = "/";
    return path;
  }

  function validateHashRouteUser(hash) {
    const user = new UserStore().getUser();
    if (!user && hash === "#/profile") hash = "#/login";
    if (user && hash === "#/login") hash = "#/";
    return hash;
  }

  return {
    router() {
      let path = window.location.pathname;
      path = validateRouteUser(path);
      const route = ROUTES[path] || ROUTES["404"];
      window.history.pushState(null, "", path);
      render(route);
    },
    hashRouter() {
      let hash = window.location.hash;
      hash = validateHashRouteUser(hash);
      const route = HASH_ROUTES[hash] || HASH_ROUTES[404];
      window.history.pushState(null, "", hash);
      render(route);
    },
    navigator(path) {
      path = validateRouteUser(path);
      const route = ROUTES[path] || ROUTES["404"];
      window.history.pushState(null, "", path);
      render(route);
    },
  };
}

막상 바꾸고 나니, routernavigator 함수는 path를 인수로 받냐, 안 받냐의 차이만 있고 로직이 동일하다.
굳이 분리하는 것보다 합치는 게 좋을 것 같다.

return {
  router(path) {
    path = path || window.location.pathname;
    path = validateRouteUser(path);
    const route = ROUTES[path] || ROUTES["404"];
    window.history.pushState(null, "", path);
    render(route);
  },
  hashRouter(hash) {
    hash = hash || window.location.hash;
    hash = validateHashRouteUser(hash);
    const route = HASH_ROUTES[hash] || HASH_ROUTES[404];
    window.history.pushState(null, "", hash);
    render(route);
  },
};

💵 트러블 슈팅: popstate 발생 시점

window.addEventListener("load", () => router());
window.addEventListener("popstate", () => router());
window.addEventListener("hashchange", () => hashRouter());

createRouter에서 생성한 routerhashRouter는 위와 같이 쓰인다.
직접 서버를 실행했는데 예상과는 다른 결과가 나왔다.
주소창에 #/login 해시 경로를 입력하고 엔터를 눌렀더니 LoginPage가 안 나오고 NotFoundPage가 나왔다.

처음에는 'load 시에 호출하는 router와 겹치는 건가?' 했는데 직접 로그를 찍어보니 hashchange 이벤트 보다 popstate 이벤트가 먼저 발생한다.

따라서 window.location.pathname/로 나오므로 HomePage를 반환하고 주소를 변경한다.
다음에 hashchange가 발생하여 window.location.hash를 찾는데 빈 문자열이므로 NotFoundPage를 반환했다.

슬랙에 남긴 질문 내용

popstate 이벤트는 브라우저에서 뒤로 가기, 앞으로 가기, 새로고침을 했을 때와 history.back(), history.forward(), history.go() 메서드 호출 시에만 발생하는 줄 알았고 찾아봐도 자료를 찾을 수 없어(마이 서칭 쉴력 ㅠㅠ) 항해 슬랙에 질문을 남겼다.

슬랙 답변 내용

다른 팀원분께서 내가 남긴 질문에 댓글을 달아주셨다.👍
간단하게 얘기하면 URL이 바뀌면 popstate가 된다는 내용이다.

그렇다면 코드에 개선이 필요하다.
현재는 createRouter에서 routerhashRouter를 내보내 이벤트에 맞게 할당했다.
하지만 popstate는 해시 변경(주소창에 직접 입력) 시에도 발생하기 때문에 상황에 따라 대처할 라우터 하나만 내보내주는 것이 좋을 것 같다.

function navigator(path) {
  if (window.location.hash) return;
  path = path || window.location.pathname;
  path = validateRouteUser(path);
  const route = ROUTES[path] || ROUTES["404"];
  window.history.pushState(null, "", path);
  render(route);
}

function hashNavigator(hash) {
  hash = hash || window.location.hash;
  hash = validateHashRouteUser(hash);
  const route = HASH_ROUTES[hash] || HASH_ROUTES[404];
  window.history.pushState(null, "", hash);
  render(route);
}

return {
  router() {
    if (window.location.hash) {
      hashNavigator();
    } else {
      navigator();
    }
  },
  navigator,
};

정리하고 나니 처음 코드보다 좋아진 건지 모르겠다...

💵 라우터 로직 다시 한 번 분리

과제 중간 Q&A 시간에 멘토님께서 다른 곳에서 SPA 프로젝트를 한다고 했을 때 갖다 써도 될 정도로 모듈화하는 것을 목표로 해보라는 말씀을 하셨다.

지금까지 정리한 코드를 살펴보자.

import MainPage from "@/pages/MainPage";
import ProfilePage from "@/pages/ProfilePage";
import LoginPage from "@/pages/LoginPage";
import NotFoundPage from "@/pages/NotFoundPage";
import UserStore from "@/store/userStore";

export function createRouter() {
  const ROUTES = {
    "/": () => MainPage(),
    "/profile": () => ProfilePage(),
    "/login": () => LoginPage(),
    404: () => NotFoundPage(),
  };

  const HASH_ROUTES = {
    "#/": () => MainPage(),
    "#/profile": () => ProfilePage(),
    "#/login": () => LoginPage(),
    404: () => NotFoundPage(),
  };

  function render(route) {
    const root = document.getElementById("root");
    root.innerHTML = route();
  }

  function validateRouteUser(path) {
    const user = new UserStore().getUser();
    if (!user && path === "/profile") path = "/login";
    if (user && path === "/login") path = "/";
    return path;
  }

  function validateHashRouteUser(hash) {
    const user = new UserStore().getUser();
    if (!user && hash === "#/profile") hash = "#/login";
    if (user && hash === "#/login") hash = "#/";
    return hash;
  }

  function navigator(path) {
    if (window.location.hash) return;
    path = path || window.location.pathname;
    path = validateRouteUser(path);
    const route = ROUTES[path] || ROUTES["404"];
    window.history.pushState(null, "", path);
    render(route);
  }

  function hashNavigator(hash) {
    hash = hash || window.location.hash;
    hash = validateHashRouteUser(hash);
    const route = HASH_ROUTES[hash] || HASH_ROUTES[404];
    window.history.pushState(null, "", hash);
    render(route);
  }

  return {
    router() {
      if (window.location.hash) {
        hashNavigator();
      } else {
        navigator();
      }
    },
    navigator,
  };
}

이 코드에서 찾은 문제점은 아래와 같다.

  1. 사용자 로그인 검증을 하고 있다.
  2. 경로 기반 라우터와 해시 기반 라우터의 분기처리를 하고 있다.
  3. 라우터 경로에 대한 페이지 설정이 하드 코딩되어 있다.

2번은 라우터의 역할이 아닌가 고민해봤는데 실제로 react-router-dom을 사용할 때, 브라우저 라우터를 사용할지 해시 라우터를 사용할지는 개발자가 별도로 선언하여 사용하는 걸로 알고 있어서 외부로 분리하기로 했다.

let routes = {};
let hashRoutes = {};

export function createRouter() {
  function addRoutes(path, element) {
    routes = { ...routes, [path]: element };
  }

  function addHashRoutes(path, element) {
    hashRoutes = { ...hashRoutes, [path]: element };
  }

  function render(pageComponent) {
    const root = document.getElementById("root");
    root.innerHTML = pageComponent();
  }

  function navigator(path) {
    const pageComponent = routes[path] || routes["404"];
    window.history.pushState(null, "", path);
    render(pageComponent);
  }

  function hashNavigator(hash) {
    const pageComponent = hashRoutes[hash] || hashRoutes["404"];
    window.history.pushState(null, "", hash);
    render(pageComponent);
  }

  return {
    navigator,
    hashNavigator,
    addRoutes,
    addHashRoutes,
  };
}

위처럼 수정하고 나니 고민이 또 생겼다.

  1. 경로 기반 라우터와 해시 기반 라우터 내부 로직이 똑같은데, 의미상 나누는 게 맞을까?
  2. 모듈로 분리하긴 했지만 routeshashRoutes를 모듈 상의 전역 변수로 선언하는게 맞을까?

이 고민 역시 다른 분들의 의견을 들어보고자 슬랙에 올렸다.

슬랙에 올린 질문

정성스럽게 댓글들을 달아주셨는데(다시 한 번 감사합니다), 댓글의 내용을 읽고 '왜 createRouter라는 이름을 사용하고 함수로 내보내고 있을까?'라는 생각이 들었다.
createRouter라는 이름의 함수는 const router = createRouter();처럼 사용할 것 같지만 막상 내보내는 반환값은 addRoutesnavigator를 메서드로 갖는 객체를 반환한다.
이 객체를 router 변수에 담고 사용할 수도 있지만 변수명은 다르게 작성할 수도 있고 구조 분해 할당으로 받는다면 더욱 이름의 의미가 퇴색될 것 같다.

createRouter.jsRouter.js로 변경하고 내부를 싱글톤 패턴의 클래스로 작성했다.
이로써 2번에서 고민한 내용은 클래스의 필드값으로 갖게 되어 고민을 해소할 수 있었다.

하지만 우연히 책을 보다가 발견한 내용인데, "모듈에서 공개적으로 내보내진 메서드는 내부 모듈 세부 사항에 대한 클로저를 유지한다. 이를 통해 프로그램이 살아 있는 동안 모듈 싱글톤의 상태가 유지된다."는 내용이었다.

'모듈로 분리한 것부터 싱글톤이면 굳이 클래스를 사용할 필요가 없겠네?'

let routes = [];
let target = null;

function render(route) {
  target.innerHTML = route.element();
}

export function setRenderTarget(element) {
  target = element;
}

export function addRoutes(path, element) {
  routes = [...routes, { path, element }];
}

export function navigator(path) {
  const route =
    routes.find((route) => route.path === path) ||
    routes.find((route) => route.path === "*");
  window.history.pushState(null, "", path);
  render(route);
}

render 함수를 라우터 로직에서 분리하고 싶었는데 여러 방법을 고민하다가 가만히 두기로 했다.

첫 번째로 'render 함수 자체를 다른 파일로 분리할까?'라고 고민했지만 라우터에서만 사용하고 있어서 큰 의미가 있을까 싶어서 패스.

두 번째로 '옵저버 패턴처럼 사용할까?'라고 생각했는데, 구현을 하다보니 라우터에 naviagtor 실행 후 실행할 함수를 콜백으로 받아야 하는데, 'render가 라우터의 역할에 맞지 않아서 분리하는 건데 render를 빼고 subscribe 함수를 넣는 건 역할에 맞나?'라는 고민이 생겨서 패스.

결국 render할 대상을 외부에서 주입받도록 하는 걸로 타협했다.

let target = null;

function render(pageComponent) {
  target.innerHTML = pageComponent();
}

export function setRenderTarget(element) {
  target = element;
}

💵 레이아웃 만들기

render 분리에 실패했으니 다른 거라도 해야겠다는 생각으로 개선할 점을 찾아봤다.
MainPageProfilePage는 같은 레이아웃을 공유하고 있는데 별도로 호출하고 있어서 레이아웃을 만들기로 했다.

import Footer from "@/components/layout/Footer";
import Header from "@/components/layout/Header";

const Layout = (children) => `
  <div class="bg-gray-100 min-h-screen flex justify-center">
    <div class="max-w-md w-full">
      ${Header()}
      ${children()}
      ${Footer()}
    </div>
  </div>
`;

export default Layout;

먼저 레이아웃 컴포넌트를 작성한 뒤 addRoutes 함수를 바꿔줬다.
레이아웃을 어떻게 추가하고 관리하는게 좋을까?
잘 생각나지 않아서 react-router-dom은 어떻게 사용하는지 찾아봤다.

<Routes>
  <Route element={<Layout/>}>
    <Route path="/" element={<MainPage/>}/>
    ... 다른 페이지들
  </Route>
</Routes>

위 코드와 같이 쓴다는 걸 확인하고 내 코드에는 어떻게 적용할 수 있을지 고민했다.
Layoutelement로 갖고 있는 Route 컴포넌트가 적용될 페이지 컴포넌트를 자식 형태로 갖고 있으니 children 배열에 넣어서 담으면 어떨까?

// main.js
addRoutes(
  {
    element: Layout,
    children: [
      { path: "/", element: MainPage },
      { path: "/profile", element: ProfilePage },
      { path: "#/profile", element: ProfilePage },
      { path: "#/", element: MainPage },
    ],
  },
  { path: "/login", element: LoginPage },
  { path: "#/login", element: LoginPage },
  { path: "*", element: NotFoundPage },
);

// router.js
export function addRoutes(...newRoutes) {
  routes = [...routes, ...newRoutes];
}

routes의 내용이 변경됐으니 navigator 함수도 변경해줘야 한다.
Layoutpath 속성을 넣게 되면 혹여나 path가 일치하는 상황이 발생할 수도 있을 것 같아서 넣지 않았다.

function findRoutes(path, routeList) {
  for (const route of routeList) {
    if (route.path === path) return route.element;
    if (route.children) {
      const layout = route;
      for (const child of route.children) {
        if (child.path === path) {
          return () => layout.element(child.element);
        }
      }
    }
  }
  return routeList.find((route) => route.path === "*").element;
}

export function navigator(path) {
  const pageComponent = findRoutes(path, routes);
  window.history.pushState(null, "", path);
  render(pageComponent);
}

navigator에서 pageComponent를 찾는 함수를 findRoutes 함수로 분리했다.
findRoutes 함수에서 path가 일치하면 element를 바로 반환하고 children 속성이 있다면 다시 반복문을 돌려 path를 검사한다.
routes의 모든 데이터를 순회해도 일치하는 pageComponent가 없다면 NotFoundPage를 반환하도록 했다.

💰 과제를 진행하며 아쉬웠던 점: 모르는 게 너무 많다.


과제를 진행하며 아쉬웠던 점은 과제에 대한 아쉬움보다 과제를 진행하며 느낀 나에 대한 아쉬움이다.
과제를 하다가 모르는 게 있으면 질문을 남기거나 zep에서 다른 항해원분들과 여쭤보고 있다.
다들 정성스럽게 대답을 해주시고 의견을 주셔서 정말 감사할 따름이다.
하지만 모르는 게 많다보니 대답해주신 의도를 잘 파악했나 싶을 때가 있다.
댓글에 답글을 모두 달고 있지만, 답글을 달고 매번 '아 이 얘기가 아니셨나?'하는 생각이 든다.

💵 vitest와 git commit 컨벤션

첫 발제일에 zep에서 다른 분들과 과제를 하고 있었다.
과제의 통과 기준은 테스트 코드 여부이다.
요구 사항을 보고 그냥 구현하자니 생각이 많아지고 어떻게 접근해야 할지 막막했다.
따라서 테스트 코드 1번부터 하나씩 통과하는 전략으로 바꿔 진행했다.

하지만 이 전략도 테스트 코드 1번 부터 차례대로 실행하고 싶은데 npm run test 명령어를 실행하면 테스트 코드 전체가 실행되어 어려움을 겪었다.
그러다 학습 메이트 분께서 VSCODE의 확장 프로그램인 vitest를 알려주셨다.
덕분에 테스트 코드를 하나씩 실행하며 과제를 진행할 수 있었다.

추가로 다른 분께서 화면 공유를 키고 과제를 진행하고 계셨는데 학습 메이트 분께서 "깃 커밋 메세지 저렇게 남겨주시면 너무 좋아요."라고 말씀하셔서 화면을 염탐했다.
그 분의 커밋 메세지를 보고 구글에 "깃 커밋 컨벤션"을 검색하여 커밋 메세지를 작성했다.

주변에 개발자가 없어서 그런걸까 누구에게 당연한 것도 모르고 있다는 사실에 조금 위축됐다.
마음 같아서는 잘 하는 분 옆에 찰싹 달라붙어 무슨 단축키를 사용하는지, 코드를 짤 때는 어떤 생각을 하는지, 키보드는 어떤 걸 쓰는지 하나하나 들여다 보고 싶다.
낯을 가린다고 혼자 고민하기 보다 여러 곳에 나를 노출시켜야 할 필요성을 느꼈다.

💵 예리하지 않은 감각

과제를 진행하면서는 "테스트 코드 통과"에 중점을 뒀다.
일찍 시작해서 조금 이르게 과제를 끝내고 코드가 지저분하다고 생각돼 리팩토링을 진행하고 있었다.
그러다 코어 타임(우리팀과 협력팀이 특정 시간에 zep에 접속하여 활동하는 시간)에 다른 분들과 과제 얘기를 했다.

다른 분들은 단순히 과제를 통과하는 것이 아니라 확장성을 고려하고 과제의 의도를 파악하고 설계를 하고 계셨다.
나는 생각하지 못했던 점들을 캐치해서 고민하는 모습에 '나는 왜 예리하게 느끼지 못했을까'라는 생각을 했다.

하지만 돌이켜 생각해보면 천천히 고민하면서 풀었어도 그런 고민을 했을까 싶다.
프론트엔드 프로젝트 경험이 없다보니 경력이 있으신 분들에 비해 생각할 풀도 적을 수 있고(합리화일 수 있고) 오히려 과제를 빨리 마친 덕분에 시간을 갖고 천천히 리팩토링을 진행할 수 있었다.

과제 중간 Q&A 시간에 멘토님께 코드를 짜기 전 어떤 동작을 할 거다라고 생각한 뒤 어떻게 코드를 작성해야겠다는 판단이 늘기위해 공부할 책을 추천해주실 수 있는지 질문드렸다.

멘토님께서 다음 챕터인 클린 코드의 내용이 들어간 "쏙쏙 들어오는 함수형 프로그래밍"책을 추천해주셨다.

💵 테스트 코드

나는 무려 테스트 코드도 작성해본 적이 없다.
자랑은 아니지만 이번에 과제를 보면서 E2E 테스트라는 것도 처음 알았다.
테스트 코드를 실행시키면서 오류가 발생했을 때 어떤 동작을 기대하는지 코드를 보고도 몰랐던 경우가 있었다.
항해 과정 중에 테스트 코드 챕터가 있지만 그 전에 한 번 공부해야겠다는 생각이 들었다.
마침 좋은 강의도 추천받았다.

인프런 프론트엔드 테스트 강의

이 강의도 다다음 챕터인 테스트 코드의 내용이 들어가있다고 한다.
챕터가 들어가기 전에 완강을 목표로 해야겠다.

💰 마치며


💵 Keep: 현재 만족하고 계속 유지할 부분

'과제에 대한 고민없이 테스트 코드 통과에 우선을 두고 진행하는 게 맞을까?'라는 고민을 했었다.
블로그 글을 쓰다가 문득 글쓰기에 관해서 여기저기서 들은 내용이 떠올랐다.

한 번에 완벽한 글을 작성하는 것은 힘들다고 한다.
일단 생각나는 대로 적고 퇴고의 과정을 여러 번 반복하며 좋은 글이 탄생한다고 한다.
단문으로 써라, 접속사를 줄여라 등등 글쓰기 관련 내용을 보면 항상 나오는 얘기를 퇴고를 하며 적용해본다.
다른 글에서 본 표현도 써보고 할 수 있다면 다른 사람에게 보여줘 여러 의견을 받는다.
자꾸 적용하다보면 습관이 되어 초고를 작성할 때의 기본 필력이 상승한다.

코드에도 통하지 않을까?
일단 기능이 되게 작성하고, 학습을 하며 리팩토링하고, 다른 사람들은 어떻게 작성했는지 보다보면 좋아지지 않을까?
고민을 하다가 될 것 같다고 판단했다.
'자료 학습 -> 과제 통과'보단 '과제 통과 -> 자료 학습 -> 리팩토링'으로.

💵 Problem: 개선이 필요하다고 생각하는 문제점

하루에 의미없이 소비하는 시간이 많다.
다른 항해원분들은 대부분 직장을 다니며 항해를 병행하고 있다.
백수로서 남들보다 시간을 확보할 수 있다는 메리트를 갖고 있다.
이 시간을 잘 활용하고 있을까?

고민해보면 아닌 것 같다.
잠도 많아지고, 생활 패턴도 불규칙해졌다.

💵 Try: 문제점을 해결하기 위해 시도해야 할 것

규칙적인 생활 패턴을 가져야겠다.
회사생활을 하는 것처럼 특정 시간은 일한다고 생각하고 과제를 하거나, 관련 학습을 해야겠다.
집중이 안돼서 다른 짓을 하더라도 책상에 있는 시간을 정해두자.

4개의 댓글

comment-user-thumbnail
2024년 12월 22일

열심히 하시는 원정님 보면 저도 타오르게 되네요.... 원정님이 많은걸 얻고 가셨으면 좋겠습니다!!

1개의 답글
comment-user-thumbnail
2024년 12월 22일

원정님의 고민하는 의식의 흐름(?)을 볼 수 있어 너무 좋네요 저도 원정님처럼 꾸준히 기록하는 습관을 배워보겠습니다..!!

1개의 답글

관련 채용 정보