[오픈소스] 직접 라이브러리 만들기 : localstorage-query

park·2024년 8월 21일
5

오픈소스

목록 보기
3/4

라이브러리 npm

라이브러리 깃헙

들어가면서

직접 라이브러리를 만들어보고 싶다는 욕망은 늘 가슴 한켠에 있었다. 그런데 어떻게 시작해야할지, 뭘 만들어야할지를 생각하면 머리가 복잡해져 호기롭게 열었던 코드에디터를 다시 꺼버리곤 했다.

그러다 '시나브로 자바스크립트'라는 강의가 런칭되었다는 소식이 들렸다. 프론트엔드에서 핫한 키워드(SSR, 하이드레이션 등)들을 직접 바닥부터 구현해보면서 그 원리를 이해하는 강의였다. 커리큘럼에는 라이브러리를 직접 만들어보는 파트도 있었다. 이 강의를 구매했고 얼마전에 완강했다.

강의를 듣던 중, 어떤 라이브러리를 만들면 좋을 지에 대해서도 열심히 생각해봤다. 거창한 거 말고 내가 개발할 때 직접 사용할만한 걸 만들고 싶었다. 그때 리액트 쿼리가 내 머리를 스쳐지나갔고 동시에 리액트에서 로컬스토리지를 이용해 작업할 때 반복되는 코드가 떠올랐다. '리액트 쿼리와 비슷한 인터페이스를 가진, 리액트에서 로컬스토리지를 사용할 때 편하게 작업할 수 있는 hook을 라이브러리로 만들면 어떨까?'

라이브러리를 만드는 방법도 알았고, 아이디어도 떠올랐다. 남은 건 실행에 옮기는 것이었다.

개발 방법론에 대하여

라이브러리를 만들 결심을 했을 때 까지만 해도 나는 '정답'에 목마른 사람이었다. 무의식적으로 최선의 방법은 반드시 존재한다고 믿고 있었고 그것은 결국 나를 '방법'에 매몰되게끔 했다. 더 멋진 방법, 더 있어보이는 방법, 실무에서 사용한다는 휘황찬란한 가지각색의 방법들...

그러다 라이브러리를 만들면서 깨달은 것이 있다. 정답 따윈 없다는 것과 방법에 매몰되는 것의 위험성이다. 방법에 매몰되면 정작 가장 중요한 목표는 잊어버리기 쉽상이다. 즉, 유용한 도구를 만들겠다는 본질은 저편에 미루게 된다. 익숙치 않은 개념들에 허덕이느라. 그러다보면 지루해지고 그만두게 된다.

따라서 일단 가장 쉽고 편하며 빠르게 시작할 수 있는 방법으로 시작해 목표를 달성하는 것이 나의 개발 방법론이다. 물론 그렇게 목표를 달성하는 과정에서 발생하는, 비효율성이나 성능저하와 같은 문제들을 그냥 지나쳐서는 안될 것이다. 그러면 그때 그런 문제들을 개선하면 된다. 그러면 나는 더 나은 방법들을 배우게 될 것이다. 그리고 다음 프로젝트에는 더 나은 방법들을 바로 적용하게 될 것이다.

그리고 우리가 아는 위대한 소프트웨어들도 처음부터 그런 모습이진 않았을 것이다. 모든 것들이 차곡차곡 쌓아올린 결과물이다.

너무 멀리 보지 말자. 현재의 상황에 집중하자. 있을리 없는 정답을 찾느라 허덕이고, 방법에 매달리는 행위는 내가 생각하기엔 번아웃을 유발하고, 길을 잃게 만드는 것 같다.

스텝 1. 요구사항 정의

어떤 것을 만들 것인가? 이것부터 명확히 정의하고 넘어가야했다.

일단 다음과 같이 써내려갔다.

이 라이브러리의 목표: 리액트를 사용할 때 로컬스토리지를 더 간단하게 쓰기 위해서

  1. 리액트 라이브러리로 설치할 수 있어야 함(hook)
  2. key를 전달해주면 원하는 값을 원하는 타이밍에 구독할 수 있어야함.
  3. 사용자가 구독할 값의 초기값을 설정할 수 있어야함
  4. 사용자가 값을 변경할 수 있어야 함
  5. 사용자가 해당 값을 삭제할 수 있어야 함
  6. 사용자가 로컬스토리지를 완전히 비울수 있어야함

그런데 그냥 이렇게 줄글로만 써내려가니 이 기능이 확실히 필요한지 필요없는지, 서로 충돌하는 기능은 없는지 애매하게 느껴졌다.

특히, 2번 기능의 '원하는 타이밍에 구독'이 굳이 필요한가? 의문이 들었고, 6번 기능도 필요한지 의문이 들었다.

이렇게 고민만하다가는 영영 라이브러리를 만들지 못할거란 생각에 직접 검증을 하기로 했다. 앞서 언급했듯, 내가 당장 시행할 수 있는 방법으로.

그건 바로 일단 대충 비슷하게 구현해보기였다. 어차피 복잡한 로직이 필요한 건 아닐테니, 금방 만들 수 있을 것이었고 빠르게 논리적인 검증도 가능할 것이었다.

코드로 구현하면서, 옆에 주석을 달아가며 의사결정을 해나갔다. 작성한 코드들과 고민의 흔적들을 그대로 공개해보자면...

import { useEffect, useState } from 'react';

// 나는 뭘 하고 싶냐?
// useLocalstorageQuery(key) -> {data, mutate, remove, clear}
// data : key로 getItem한다음 Parse해서 보여줄거야.
// mutate: key로 getItem한다음 Parse한후에 변경하려는 형태로 변경하고 setItem 해줄거야.
// mutate key로 remove 해줄거야.
// clear : static 속성 마냥 빼는게 좋을 것 같음.

export default function useLocalstorageQuery<T>(key: string, initialValue?: T) {
  const [data, setData] = useState<T | null>();
  const stringifiedInitialValue = initialValue
    ? JSON.stringify(initialValue)
    : '';

  useEffect(() => {
    const value = localStorage.getItem(key);
    if (value !== null) {
      const parsed = JSON.parse(value);
      setData(parsed);
    } else {
      if (stringifiedInitialValue !== 'undefined') {
        localStorage.setItem(key, stringifiedInitialValue);
        setData(JSON.parse(stringifiedInitialValue));
      } else {
        localStorage.setItem(key, JSON.stringify(null));
        setData(null);
      }
    }
  }, [key, stringifiedInitialValue]);

  // initialValue로 [] 넣었을 때 왜 무한 리렌더링?? -> 아마 initialValue 때문인것 같은데...
  // setData -> data 사용하는 App 리렌더링 -> 이 hook도 리렌더링 -> 새로운 배열 객체 주소 들어옴 -> 무한 리렌더링
  // 그렇다면 []를 string으로 미리 바꿔버리자.

  // const subscribe = () => {
  //   // 이름을 init으로 바꿔야할지도..
  //   // 얘가 필요한가? 타이밍을 사용자가 결정하게 하는 게 맞는가?
  //   const value = localStorage.getItem(key);
  //   if (value === null) {
  //     localStorage.setItem(key, initialValue ? initialValue : null);
  //     setData(initialValue ? initialValue : null);
  //     return;
  //   } // 이미 있는 값을 구독할 때는 어떻게?
  // 생각해보니 유저의 입장에서는 어떤 키에 대한 값을 명시적으로 유저 스스로 구독하는 건데(초깃값 설정해주거나 말거나 그것은 유저의 선택) 또 구독 시점을 정해야한다는 건 번거로운 것 같음)
  // };

  const mutate = (newValue: T) => {
    const value = localStorage.getItem(key);
    if (value !== null) {
      localStorage.setItem(key, JSON.stringify(newValue));
      setData(newValue);
    }
  };

  const remove = () => {
    const value = localStorage.getItem(key);
    if (value !== null) {
      localStorage.removeItem(key);
      setData(null);
    }
  };

  return {
    data,
    mutate,
    remove,
  };
}

// 얘는 없앨수도..
// const clear = () => {
//   localStorage.clear();
// };
// 생각해보면 이 훅의 목적은 하나의 키에 대한 값의 상태 관리인데 다 지우는 걸 제공하는 건 이상함. + 이렇게 구현하면 state에 반영안됨

// useLocalstorageQuery.clear = clear;

이렇게 하다보니 그림이 그려졌고 생각이 분명해졌다.

2번 기능에 대해서는 유저의 입장에서는 어떤 키에 대한 값을 명시적으로 유저 스스로 구독하는 건데 또 구독 시점을 정해야한다는 건 번거로운 것 같아서 빼는 게 낫겠다는 생각이 들었다.

6번 기능에 대해서는 이 훅의 목적은 하나의 키에 대한 값의 상태 관리인데 다 지우는 걸 제공하는 건 이상하다는 생각이 들었다.

이러한 판단 아래 2번, 6번 기능은 빼기로 결정했다.

따라서 최종적인 기능 요구사항은 다음과 같다.

  1. 리액트 라이브러리로 설치할 수 있어야 함(hook)
  2. key를 전달해주면 원하는 값을 구독할 수 있어야함
  3. 사용자가 구독할 값의 초기값을 설정할 수 있어야함
  4. 사용자가 key에 대한 값을 변경할 수 있어야 함
  5. 사용자가 key에 대한 값을 삭제할 수 있어야 함

스텝 2. 구현

시험삼아 구현해본게 있어서 실제로 구현하는 것은 어렵지 않았다.

import { useEffect, useState } from 'react';

export default function useLocalstorageQuery<T = undefined>(
  key: string,
  initialValue?: T | null
) {
  const [data, setData] = useState<T | null>();

  const stringifiedInitialValue =
    initialValue !== undefined
      ? JSON.stringify(initialValue)
      : JSON.stringify('undefined');

  useEffect(() => {
    const value = localStorage.getItem(key);

    const notNullValueAndNotUndefined =
      value !== null && value !== JSON.stringify('undefined');

    if (notNullValueAndNotUndefined) {
      const parsed = JSON.parse(value);
      setData(parsed);
      return;
    }

    const notNullValueButUndefined =
      value !== null && value === JSON.stringify('undefined');

    if (notNullValueButUndefined) {
      return;
    }

    if (stringifiedInitialValue !== JSON.stringify('undefined')) {
      localStorage.setItem(key, stringifiedInitialValue);
      setData(JSON.parse(stringifiedInitialValue));
      return;
    }

    localStorage.setItem(key, JSON.stringify('undefined'));
  }, [key, stringifiedInitialValue]);
  
  

  const mutate = (newValue: T) => {
    const value = localStorage.getItem(key);

    const stringifiedNewValue =
      newValue === undefined
        ? JSON.stringify('undefined')
        : JSON.stringify(newValue);

    if (value !== null) {
      localStorage.setItem(key, stringifiedNewValue);
      setData(newValue);
    }
  };

  const remove = () => {
    const value = localStorage.getItem(key);
    if (value !== null) {
      localStorage.removeItem(key);
      setData(null);
    }
  };

  return {
    data,
    mutate,
    remove,
  };
}

다만 이렇게 구현하기까지 몇 가지의 결정을 내려야했고, 하나의 큰 시련(?)을 맞이해야 했다.

사용자가 key는 줬는데 initialValue는 주지 않은 경우, 로컬스토리지에는 어떤 값을 넣어줘야하는가?

initalValue도 필수로 넣게끔 해버릴 수도 있었지만 사용자에 따라 나중에 값을 결정하고 싶을 수도 있기에 옵셔널로 뒀다. 그렇다면 사용자가 key는 줬는데 initialValue는 주지 않은 경우, 로컬스토리지에는 어떤 값을 넣어줘야하는가? 생각해본 후보는 null, 빈문자열, undefined이다.

  • 후보 1. null -> 로컬스토리지는 어떤 key-value에 대한 정보를 아예 갖고 있지 않은데 해당 값을 가져오려고 하는 경우(getItem) null을 반환한다. 따라서 null을 쓰는 것은 혼란만 가중시킨다. 그리고 의미적으로도 딱히 적절하지는 않은 것 같다.

  • 후보 2. 빈문자열 -> 조건문 안에 들어갔을 때 Boolean으로 형변환이 되므로 내가 예상치 못한 동작을 할 가능성이 크며, 사용자의 입장에서도 예상치 못할 동작을 할 가능성이 크고 사용자에게 모호함을 줄 수 있으며 사용자가 원치 않는 유형의 초기값일 가능성이 크다.

  • 후보 3. undefined -> 사용자가 값을 넘겨주지 않기로 결정한 것이기에 사용자에게 혼란을 줄 가능성이 적고 의미적으로도 적절하다. 내 입장에서도 형변환 같은 것 때문에 예상치 못한 동작을 할 가능성이 적다. 따라서 이것으로 당첨. 다만, JSON.parse는 undefined는 물론 JSON.stringify(undefined)도 인자로 못받기 때문에 JSON.stringify('undefined')로 해줘야한다. 이렇게하면 사용자 입장에서 아무 인자도 주지 않았을 때 data의 타입이 'undefined'인지, undefined인지 헷갈릴 수 있는데, 이때는 타입이 undefined가 될 수 있도록 주의해서 구현한다.

이미 해당 key에 대한 value가 있을 경우, initialValue를 넣었을 때 어떻게 동작하게 할 것인가?

해당 initialValue를 무시하고 기존값을 쓰는 것으로 결정했다. 기존값의 변형에 의한 혼란을 방지하기 위함이다. 그러나 오히려 무시하도록 한 것이 사용자 입장에선 직접 로컬스토리지를 수동으로 지워줘야하기 때문에 불편할 수도 있고 당황스러울수도 있다. 따라서 initialValue를 무시하지 않도록 변경할 수도 있고 아니면 콘솔에 경고 메시지 같은 것을 출력하여 사용자에게 왜 이렇게 동작하는지를 알릴수도 있다.

현재는 후자로 마음이 기우는 상황이다.

무한 리렌더링 지옥에 갇히다

소스코드의 이 부분을 주목해보자.

// ...
const stringifiedInitialValue =
    initialValue !== undefined
      ? JSON.stringify(initialValue)
      : JSON.stringify('undefined');
// ...

stringifiedInitialValue를 굳이 왜 만들었는가? 스트링화하지 않으면 useEffect 안에서 무한 리렌더링 지옥에 갇히기 때문이다. 원시값은 상관없는데 만약 초기값으로 객체나 배열을 넣었을 경우,

setData를 어디에선가 한다 -> 이 data를 사용하는 컴포넌트가 리렌더링된다 -> useLocalstorageQuery도 리렌더링된다 -> initialValue로 계속 새로운 객체 주소가 들어온다 -> 이 initialValue에 의존하는 useEffect가 계속 트리거된다 -> 무한 리렌더링 지옥

따라서, 초기값으로 들어온 값이 무엇이든 JSON.stringify를 통해 스트링화 및 직렬화시킨다. (localStorage에 저장하기 위해 어차피 수행해야하는 작업이기도 하고..) useEffect 밖에서 수행해야한다. 그리고 useEffect는 initialValue대신 stringifiedInitialValue에 의존하게 되는데, 원시값이므로 무한 리렌더링이 촉발되지 않는다.

참고로 관련해서, 이 훅을 사용하는 사용자는 다음과 같은 상황을 유의해야한다.

App.tsx

  function App(){
    
    // ...
  	const { data } = useLocalstorageQuery<TodoItem[]>('todo', [
    {
      title: '밥먹기',
      completed: false,
      id: uuidv4(), // uuidv4는 스트링 형태의 임의의 id를 리턴하는 함수
    },
  ]);
    
    // ...
  
  }

위의 경우 initialValue로 받은 배열을 스트링화 시켰더라도 무한 리렌더링이 발생한다.

setData를 어디에선가 한다 -> 이 data를 사용하는 App이 리렌더링된다 -> useLocalstorageQuery도 리렌더링된다 -> stringifiedInitialValue로 id가 다른 스트링화된 배열이 들어온다 -> 이 stringifiedInitialValue에 의존하는 useEffect가 계속 트리거된다 -> 무한 리렌더링 지옥

id를 동적으로 생성해줘서 발생하는 문제인 것이다. 사용자는 이 훅을 사용할 때 매 렌더링마다 새롭게 생성되는 요소가 없도록 유의해야 한다.

사용자가 이와 같은 상황을 맞이했을 경우 디버깅을 할 때 지게될 인지적 부담을 줄이기 위해, 관련된 경고 메시지를 콘솔에 출력해주는 기능을 구현해주면 어떨지 생각 중이다.

스텝 3. 라이브러리로 배포할 준비

리액트를 최종 파일에서 제외

사용자가 이 라이브러리를 사용할 때는 무조건 리액트를 설치할 것이 분명하므로, 라이브러리에 굳이 리액트를 포함할 필요는 없다. (참고)

따라서 다음과 같이 번들러 옵션에서 리액트는 번들된 파일에 포함시키지 말라고 명시했다.

import { resolve } from 'path';
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.tsx'),
      name: 'LocalstorageQuery',
      fileName: 'localstorage-query',
      formats: ['es'],
    },
    rollupOptions: { // 이 부분!
      external: ['react'],
    },
    emptyOutDir: false,
  },
});

그래도 혹시 만일의 경우를 대비해, 즉 사용자가 리액트를 설치하지 않고 이 훅을 사용하려고 할 경우를 대비해 peer dependency를 pacakge.json에 명시해주었다. 일단 18버전 이후의 react가 필요하다고 작성했다. 이 훅이 사용한 리액트의 버전이 18버전이라 혹시 또 뭐가 안맞을 수 있다는 불안감에 18버전이라고 설정했다.

타입 뽑아내기

전역적인 타입 선언 파일(.d.ts)을 소스코드로부터 뽑아내기 위해 tsconfig에서 다음 옵션을 조정했다.

  • noEmit: 타입스크립트로 어떤 아웃풋도 생성하지 않겠다는 설정. 지워버림
  • declaration: 타입스크립트에서 타입을 뽑아내겠다는 뜻. true로 설정
  • emitDeclarationOnly: 타입만 뽑아내겠다는 뜻. true로 설정
  • outDir: 뽑아낸 타입이 들어갈 output 디렉토리. "./dist"로 설정

이렇게 세팅한 후, build 명령을 실행했는데 무슨 짓을 해도 타입 파일이 dist 폴더에 생성되지 않았다.

원인을 삽질하면서 탐구해본 결과, 빌드 명령어 순서가 1차적 원인이었다.

tsc -b && vite build

이렇게 하게되면 vite build가 tsc가 만들어낸 결과물을 덮어씌워버린다.

따라서 tsc는 어차피 타입 파일만 생성하는거라 빌드 결과물엔 영향을 주지 않을 것 같아 두 명령어의 순서를 바꿨다. 다행히 원하는대로 잘 작동했다.

그런데 명령어의 순서를 바꾸는게 영 찝찝했다. 타입 검사를 한 후에 빌드를 하는 게 아무래도 맞다. 그래서 찾아보니, vite가 dist 폴더를 비우지 않도록 할 수 있었다. 바로 emptyOutDir 옵션이었고 이것을 false로 설정한다. 이렇게 하니 최종적으로 명령어의 순서를 바꾸지 않고도 타입을 뽑아낼 수 있었다.

스텝 4. 테스트 - 예제 코드 만들기

라이브러리를 배포할 수 있는 단계가 되긴 했는데, 이 라이브러리가 잘 작동하는지 테스트할 필요가 있었다.

먼저 vitest를 떠올렸지만, localStorage를 모킹해야하는 번거로움이 있었다.

나는 당장 빠르게 라이브러리의 기능을 테스트하고 싶었고, 눈에 직접 보이는 결과로 테스트하고 싶었다. 따라서 직접 예제 코드를 짜서(예제코드는 어차피 사용자에게 보여주기 위해 필요한 것이기도 하고...) 라이브러리의 기능을 테스트하기로 했다.

그래서 뭘 만들었느냐.간단한 투두리스트 앱이다.

소스 코드는 다음과 같다.

import useLocalstorageQuery from '@confidential-nt/localstorage-query';
import { FormEvent, useRef } from 'react';
import { v4 as uuidv4 } from 'uuid';
import styles from './App.module.css';

type TodoItem = {
  title: string;
  completed: boolean;
  id: string;
};

const key = 'todo';
const uuid = uuidv4(); // 매 렌더링마다 새롭게 생성되는 요소가 없도록 유의해야 합니다. 그렇지 않으면 내부적으로 무한 리렌더링이 일어나게 됩니다.

export default function App() {
  const { data, mutate, remove } = useLocalstorageQuery<TodoItem[]>(key, [
    {
      title: '밥먹기',
      completed: false,
      id: uuid,
    },
  ]); // key에 해당하는 값이 로컬 스토리지에 없을 경우, 주어진 initial value로 즉시 초기화됩니다. 그렇지 않은 경우 무시됩니다.

  const inputRef = useRef<HTMLInputElement>(null);

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    if (inputRef.current && data) {
      const newTodo = {
        title: inputRef.current.value,
        completed: false,
        id: uuidv4(),
      };
      mutate([...data, newTodo]); // CRUD 모든 작업에 mutate 함수를 사용할 수 있습니다. 다만, 불변성을 유지하도록 하세요.
      inputRef.current.value = '';
    }
  };

  const handleDelete = (id: string) => {
    if (data) {
      const newData = data.filter((d) => d.id !== id);
      mutate(newData);
    }
  };

  const handleCheck = (checked: TodoItem) => {
    if (data) {
      const newTodo = {
        ...checked,
        completed: !checked.completed,
      };
      const newData = [...data];
      const index = newData.findIndex((d) => d.id === checked.id);
      newData[index] = newTodo;
      mutate(newData);
    }
  };

  return (
    <main>
      <h1>Example</h1>
      <div className={styles.container}>
        <div>
          <form onSubmit={handleSubmit} className={styles.form}>
            <input type="text" placeholder="Type Todo..." ref={inputRef} />
          </form>
          <ul className={styles.items}>
            {data &&
              data.length > 0 &&
              data.map((d) => (
                <li key={d.id} className={styles.item}>
                  <div>
                    <span>{d.title} </span>
                    <div>
                      <input
                        type="checkbox"
                        checked={d.completed}
                        onChange={() => handleCheck(d)}
                      />
                      <button onClick={() => handleDelete(d.id)}>Delete</button>
                    </div>
                  </div>
                </li>
              ))}
          </ul>
          <button className={styles.deleteAllBtn} onClick={() => remove()}>
            Delete All Todos
          </button>
        </div>
      </div>
    </main>
  );
}

물론 문제가 있다. CI/CD를 할때 적용할 수 없는 방법이다. 물론 이 방법은 CI/CD 까지 생각 안하고 그냥 지금 당장 라이브러리가 잘 돌아가는지 확인하기 위해 선택한 방법이긴 하지만... 어쨌든 나중에 진짜 테스트 코드를 추가해볼 예정이다.

스텝 5. 예제 코드 배포하기

예제 코드와 라이브러리를 쉽게 한 곳에서 관리하기 위해 후에 모노 레포를 구성했다. 예제코드는 examples/app 안에, 라이브러리는 packages/lib 안에 넣었다. 그리고 사용자가 예제 코드가 실제로 어떻게 돌아가는지 눈으로 확인했으면 좋겠어서 예제 코드를 Vercel로 배포하기로 했다.

그런데 모노레포를 배포하는 것은 처음이라 좀 헤맸다.

맨 처음엔 Vercel에다가 그냥 examples/app을 배포하라고 하면 되지 않나 싶어서 그렇게 했는데, 라이브러리 모듈을 찾을 수 없다는 에러가 떴다.

다양한 삽질을 하며 원인을 찾아냈다.

생각해보면 당연한게, 예제 코드는 라이브러리를 다음과 같이 의존하고 있다.

 "dependencies": {
    "@confidential-nt/localstorage-query": "workspace:*",
    // ..
    }

즉, 라이브러리를 npm 레지스트리를 통해 가져오는 게 아니라, 그냥 루트에 정의해준 workspace 설정을 통해 로컬에서 가져오고 있으며, 그 정확한 경로는 확인해보니 lib 내부 dist 폴더다.

그런데 lib 내부 dist 폴더는 따로 빌드를 안해주면 당연히 없는 것이다. 그러니까 당연히 라이브러리를 인식하지 못하고 있는 것이다.

이 문제는 Vercel의 세팅을 바꿔 해결했다.

루트 디렉토리 : examples/app 에서 . (그냥 루트) 로 변경

빌드 명령어: pnpm -F @confidential-nt/localstorage-query build && pnpm -F app build

output 디렉토리: ./examples/app/dist

이렇게 하니까 이제는 라이브러리를 인식하고 정상적으로 배포가 완료됐다.

마무리

이 글을 쓰기로 결정한 것이 참 잘한 일이라는 생각이 든다. 쓰면서 잠재적인 에러를 여럿 발견했고 그것들을 뜯어고쳤다.

간단한 라이브러리지만 만들면서 배운 게 많았다. 또 어딘가에 숨어있을 잠재적인 에러들을 해결하고 사용성, 비효율성을 개선해나가면서 앞으로도 많이 배울 예정이다.

profile
프론트엔드 개발자. 엔지니어가 되고 싶습니다. 개발자의 관점에서 문제를 이해하고 해결하는 것을 연습하고 있습니다.

2개의 댓글

comment-user-thumbnail
2024년 8월 23일

직접 오픈소스를 만들어보시면서 겪는 여러 경험들과, 테스트/배포 까지 어떤 방식으로 고민하셨는지 담겨있는 정말 소중한 글이네요! 감사합니다ㅎㅎ

1개의 답글