ziglog

    Search by

    4월 2주차 기록

    April 9, 2022 • ☕️☕️☕️ 17 min read

    아직 살만해요


    배워가기

    React Portal

    React portal을 이용하면 부모 컴포넌트의 DOM 계층 구조 바깥에도 자식을 렌더링할 수 있다. 주의할 점은, Portal로 렌더링한 컴포넌트는 DOM 트리에서의 위치와는 상관없이 여전히 React 트리에 존재한다는 것이다.

    따라서 Portal은 DOM 트리 어디에도 존재할 수 있지만 일반적인 React처럼 동작한다. 이것에는 이벤트 버블링도 포함되어 있어, Portal 내부에서 발생한 이벤트는 React 트리에 포함된 상위로 전파된다. 그렇기 때문에 stopPropagation()과 같이 이벤트 전파가 되지 않게 구현했을 때 실제 그려진 DOM 트리에서는 영향을 받을 수 없는 구조임에도 불구하고 동작에서는 영향을 받아, 기대와는 다르게 동작하는 경우가 생길 수 있다.

    Ref https://ko.reactjs.org/docs/portals.html

    || vs ??

    논리 OR 연산자 ||는 첫 번째 truthy 값을 반환하고, Nullish 통합 연산자 ??는 첫 번째 정의된 값을 반환한다.

    Copy
    const foo = { count: 0 };
    
    // foo.message 피연산자의 정의 유무로 결과값 반환
    console.log(foo.message ?? "bar"); // 'bar'
    
    // foo.count 피연산자가 falsy 할 경우 다음 첫번째 truthy 값을 반환
    console.log(foo.count || "없다"); // '없다'

    논리 할당에서도 같은 맥락으로 사용된다.

    Copy
    const foo = { count: 0 };
    
    foo.message ??= "bar";
    console.log(foo.message); // 'bar'
    
    foo.count ||= "없다";
    console.log(foo.count); // '없다'

    npm scripts 정복하기

    npm scripts 내 npm scripts를 호출할 때 argument를 그대로 넘겨주고 싶다면, 대시 문자(-)를 사용한다.

    Copy
    "test": "react-app-rewired test --watchAll=false",
    "test:coverage": "npm run test -- --coverage

    -를 붙이지 않으면 arguments가 전달되지 않으며, 환경변수의 경우 그대로 적용된다.

    Copy
    "test": "react-app-rewired test --watchAll=false",
    "test:coverage": "npm run test -- --coverage  ...",
    "test:ci": "TZ=Asia/Seoul JEST_JUNIT_OUTPUT_DIR=coverage npm run test:coverage -- --ci ..."

    위 예시에서, 중첩된 npm scripts는 어떻게 동작할까?

    npm@6 에서는 중첩된 npm scripts가 실패하면 실행한 npm scripts도 실패하지만, npm@8 에서는 그렇지 않다.

    • npm@6 에서는 npm run test:ci를 호출했을 때 npm run test:coverage 가 실패하면 test:ci도 실패한다.
    • npm@8 에서는 npm run test:ci를 호출했을 때 npm run test:coverage 가 실패해도 test:ci는 실패하지 않는다.

    instanceof 대신 속성 체크

    객체에 대한 타입 가드를 작성할 때, 인터페이스에 대한 instanceof 체크 대신 속성 체크를 통해 작성해야 한다.

    Copy
    if (shape instanceof Rectangle) {
      // 🚨
    }
    
    if ("height" in shape) {
      // ✅
    }

    instanceof 체크는 런타임에 일어나지만, Rectangle은 인터페이스이기 때문에 런타임 시점에 아무런 역할을 할 수 없다. 타입스크립트의 타입은 컴파일 과정에서 제거되기 때문이다. 따라서 런타임에 타입 정보를 체크하기 위해서는 객체 속성이 존재하는지 체크하면 된다.

    또는 런타임에 접근 가능한 타입 정보를 명시적으로 저장하는 ‘태그’ 기법을 이용한다. (tagged union)

    덕 타이핑 (duck typing)

    JavaScript는 덕 타이핑 기반 언어다. 덕 타이핑이란, 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 객체를 해당 타입에 속하는 것으로 간주하는 방식이다. (만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리면 그 새를 오리라고 보는 방식이다.)

    TypeScript의 타입 시스템은 자바스크립트의 런타임 동작을 모델링하며, 이를 구조적 타이핑(structural typing)이라는 용어로 이야기한다.

    예를 들어 interface A { name: string }, interface B { name: string; type: string; }가 있을 때, 인터페이스 BA와 동일한 name 속성이 있기 때문에 따로 관계를 정의하지 않아도 A 타입에 속한 것으로 간주된다.

    React render prop

    React의 render prop이란 React 컴포넌트 간에 코드를 공유하기 위해 함수 props를 이용하는 테크닉이다. 컴포넌트에서 받는 children에 특정 prop을 전달하고 싶을 때 유용하다.

    Copy
    interface AProps {
      children: ({ isValid }: { isValid: boolean }) => ReactNode;
    }
    
    const A: FC<AProps> = ({ children }) => {
      const [isValid] = useState(false);
    
      return <div>{children({ isValid })}</div>;
    };
    
    const B = () => {
      return <A>{({ isValid }) => (isValid ? 'O' : 'X')}</A>;
    };

    웹팩 devtool sourcemap 옵션

    • inline-source-map
      • 정확한 line과 column까지 알려주는 소스맵이다.
      • column까지 찾아주는게 실제 디버깅에 큰 도움이 되지는 않아도, 개발환경에서 번들의 크기는 많이 증가시킨다.
    • cheap-module-source-map
      • 정확한 line을 알려준다.
      • 웹팩에서 추천하는 옵션이다.

    props 확장하기

    아래 예시를 살펴보자.

    props에서 어떤 속성(time)이 union type을 보장하면서, 각 타입에 맞는 onChange 함수를 받아오고 싶을 때는 아래와 같이 union으로 활용하면 된다. timeTimeFormat일 경우는 string을 parameter로 받는 onChange 함수를, Date 형식일 경우는 Date를 parameter로 받는 onChange 함수를 props로 받게된다.

    Copy
    interface CommonTimePickerProps {
      time?: TimeFormat | Date;
    }
    
    interface TimeFormatProps extends CommonTimePickerProps {
      time?: TimeFormat;
      onChange(value: string): void;
    }
    
    interface DateProps extends CommonTimePickerProps {
      time?: Date;
      onChange(value: Date): void;
    }
    
    export type TimePickerProps = TimeFormatProps | DateProps;

    window beforeunload 이벤트

    beforeunload 이벤트를 사용하면 사용자가 페이지를 떠날 때 정말로 떠날 것인지 묻는 확인 대화 상자를 표시할 수 있다. 사용자가 확인을 누를 경우 브라우저는 새로운 페이지로 탐색하고, 취소할 경우 탐색을 취소하고 현재 페이지에 머무른다.

    이밖에도 form 임시저장 등에도 beforeunload를 사용할 수 있다.

    PSU

    PSU란 Pre Signed URL의 약자로, AWS S3 를 접근할 수 있는 권한(업로드를 허용해주는)을 가진 URL이다. 미리 서명된 url을 가진 사용자만 객체에 액세스 할 수 있게 해준다.

    PSU로 HTTP GET 요청을 보내면 객체를 다운로드 할 수 있다. 또 PSU로 파일을 body에 담아서 HTTP PUT 요청을 하면 파일을 업로드할 수 있다.

    React에서 제공하고 있는 유틸리티 타입들

    • React.ComponentProps

      • React 컴포넌트에서 쓰이는 props 타입들을 별도의 타입으로 선언해서 사용하고 싶을 때, type 컴포넌트Props = React.ComponentProps<typeof 컴포넌트>와 같이 작성할 수 있다.
    • React.ComponentPropsWithoutRef

      • forwardRef를 통해 ref를 전달받는 컴포넌트가 있을 때, 이 컴포넌트의 props에 대한 타이핑이 필요하다면 ref를 제외한 속성들에 대한 타입이 필요할 것이다. 이 때 이걸 사용한다.

    BFF(Backend-For-Frontend)

    BFF는 모놀리스 아키텍처가 점진적으로 변화하면서 생겨난 아키텍처 패턴이다.

    BFF가 등장하기 이전에는, 다음과 같은 문제점들이 있었다.

    • 여러 API를 병합하여 하나의 페이지를 만드는 이전 방식의 경우 단일 웹 사이트에서 수많은 Http 요청이 발생한다.
    • 공개 API만 사용하다가 자체 전용 기능을 만들려면 여러 곳에서 OAuth 범위를 확인해야 한다.

    BFF 아키텍처 도입 이후에는, 다음과 같은 장점들을 이용할 수 있게 되었다.

    • BFF에서 클라이언트에서 필요로 하는 모든 리소스를 모아서 단일 엔드포인트로 제공한다.
    • 다중 BFF(모바일, 웹)를 구성해서 빠르게 이동할 수 있다.

    그러나 BFF가 늘어나면서 각 BFF에서 데이터를 가져오고 병합하는 중복 코드도 늘어났다. 이는 마이크로 서비스와 BFF 사이에 어플리케이션 서비스를 두어 중복된 로직을 처리하는 역할을 맡기는 방식으로 해결한다.

    Ref https://philcalcado.com/2015/09/18/the_back_end_for_front_end_pattern_bff.html

    p 태그는 div 태그를 품을 수 없다.

    반대로 div 태그는 p 태그를 품을 수 있다.

    div는 phrasing content가 아니고(링크), p는 phrasing content인데 (링크), phrasing content는 phrasing content로 분류되는 요소들만 포함할 수 있기 때문이다. (링크)

    🤔 phrasing content? HTML 태그들은 그 특성에 따라 Metadata, Flow, Sectioning, Heading, Phrasing, Embedded, Interactive 총 7개의 카테고리로 분류된다. 여기서 Phrasing Content(구문 컨텐츠)는 텍스트와 텍스트가 포함된 마크업을 정의한 컨텐츠를 가리킨다.

    HTML5 스펙(content model) 상

    • divflow content에 속해서 하위에 block 요소를 쓸 수 있다.
    • pphrasing content 에 속해서 하위에 inline 요소만 쓸 수 있다.

    Ref https://stackoverflow.com/questions/8397852/why-cant-the-p-tag-contain-a-div-tag-inside-it https://abcdqbbq.tistory.com/6

    package-lock.jsonlockfileVersion

    package-lock.json 파일에는 lockfileVersion이라는 것이 있다. package-lock.json 파일을 만들 때 사용됐던 시멘틱을 갖고 있는 문서의 버전 번호를 명시한다.

    npm@7부터 package-lock.json 형식이 크게 바뀌었다. npm@7은 lockfileVersion: 2를 갖고 있다. 그 외 npm 버전들의 lockFileVersion은 다음과 같다.

    • 명시된 버전 없음: 고대의 package-lock.json
    • 버전 1: npm@6
    • 버전 2: npm@7 (하위 호환성 보장)
    • 버전 3: npm@7 (하위 호환성 보장하지 않음)

    버전 2를 쓰는게 현재 가장 무난하다고 한다. package-lock.json 버젼이 계속해서 2로 지속될 수 있게끔 팀원간 npm 버젼 맞추기, CI/CD 환경의 npm 버젼 맞추기가 필요할 것이다.

    Ref https://docs.npmjs.com/cli/v8/configuring-npm/package-lock-json#lockfileversion https://jopemachine.github.io/2021/11/09/Package-Lock-Json-Lockfile-Version/

    EAFP (It’s Easier to Ask Forgiveness than Permission) vs LBYL (Look Before You Leap)

    EAFP는 일단 수행시키고 에러가 발생하면 그 때 처리하는 방식을 말한다. (try~catch 등)

    LBYL는 어떤 코드를 실행하기 전에 에러가 발생할 만한 부분을 조건문으로 미리 체크하는 방식을 말한다. (if문 등 )

    파이썬 등의 언어는 EAFP를 권장한다고 한다. (마치 선타투후뚜맞 같다.)

    nested form (form 안에 form) 은 HTML 5에서 지원하지 않는다.

    HTML의 form은 단 한번의 HTTP 요청을 위해 만들어졌다. 만약 중첩된 form의 경우 부모 form에서 submit했을 때, 자식 form은 부모의 field이기 때문에 자식 form의 submit action이 같이 되어야 하는 것인지, 혹은 자식 form의 field들만 submit되어야 하는지 애매모호해진다.

    superstruct에서의 union + literal

    superstruct 스키마를 아래처럼 union + literalenums로 정의할 경우에 infer로 타입을 빼보면, enumsstring으로만 추론되고 union + literal을 사용한 경우 더욱 상세하게 추론된다.

    Copy
    const PSULinkRequestBodySchema = object({
      uploadType: union([literal(`CDN`), literal(`SECURE`)]),
      uploadType2: enums(["CDN", "SECURE"]),
    });
    
    type PSULinkRequestBody = Infer<typeof PSULinkRequestBodySchema>;
    
    type PSULinkRequestBody = {
      uploadType: "CDN" | "SECURE";
      uploadType2: string;
    };

    superstruct의 object/type 비교

    • object는 비교대상 객체와 정확히 일치해야 assertion을 통과한다.
    • type은 비교대상에 포함되기만 하면 assertion을 통과한다.
    • API의 응답값과 정확히 일치한지 체크하려면 object, 응답값 중 일부만 비교하려면 type을 사용한다.

    z-index 음수

    z-index가 음수인 경우 요소가 화면에서 사라지는 것이 아닌, 가장 뒤로 이동하게된다. 해당 레이어를 포함하고 있는 background color가 투명하다면, z-index가 음수여도 보이게된다. 즉, z-index가 음수일 경우는 background color에 의해 가려지는 것이었다.

    특정 부분의 뒷 배경만을 다루기 위해서 z-index 음수를 활용하는 경우가 있는데, 큰 틀 (Wrapper)같은 경우는 background color가 없는 케이스가 거의 없다보니 z-index가 음수인 요소가 background color에 가려지는 경우가 발생한다.

    이럴 때는 필요한 부분에만 새로운 투명한 쌓임맥락을 만들어서 z-index 음수를 활용하여 해결할 수 있다.

    고차 컴포넌트와 React mixins

    고차 컴포넌트(HOC, Higher Order Component)는 컴포넌트를 인자로 받아 새로운 컴포넌트를 반환하는 함수로, 컴포넌트 로직을 재사용하기 위해 사용하는 패턴이다.

    Copy
    const EnhancedComponent = higherOrderComponent(WrappedComponent);

    React mixins 역시 클래스 컴포넌트를 사용하던 당시, 컴포넌트 로직을 재 사용하기 위해 나왔던 패턴이다. 하지만 여러 문제가 있기 때문에 가능한 mixins 대신 HoC를 사용하는 것을 권장한다.

    Copy
    var PureRenderMixin = require("react-addons-pure-render-mixin");
    
    var Button = React.createClass({
      mixins: [PureRenderMixin],
      // ...
    });

    믹스인의 문제

    • 믹스인은 암시적 의존성을 만든다. 믹스인은 한 컴포넌트에 있는 파일이 아니기 때문에 종속성이 생기고, 문서화하기 어렵다.
    • 믹스인은 이름 충돌을 야기한다. 타 패키지나, 믹스인 심지어 믹스인이 적용될 컴포넌트까지 메소드명이 충돌될 수 있다.
    • 믹스인은 복잡성을 눈덩이처럼 불어나게 한다. 믹스인의 기능을 확장하면서 점점 다른 믹스인이 생겨나게 되고, 결과적으로 믹스인들끼리는 강한 종속성이 생겨버린다.

    Ref https://itmining.tistory.com/124

    CSS :is :where pseudo selector

    Working Draft Level 4에 있는 문법이지만 대부분의 모던 브라우저에서 지원하고 있다.

    :is 는 다음과 같이 동작한다.

    Copy
    div > p, div > span, div > h1 { /* 동일하게 */ }
    div > :is(p, span, h1) { /* 동작함 */ }
    
    button.focus, button:focus { /* 동일하게 */ }
    button:is(.focus, :focus) { /* 동작함 /* }

    :is:where 는 비슷하게 동작하지만, :where 는 명시도를 계산하지 않아 항상 0이라는 큰 차이점이 있다. 이는 가장 우선순위가 낮게 적용된다는 뜻으로, 어떤 위치에 정의했어도 가장 앞쪽에 정의한 스타일 속성처럼 동작한다. 그래서 CSS를 Reset하는 스타일을 작성할 때 사용하기 좋다.

    반면에 :is 는 일반적인 CSS 선택자처럼 명시도의 영향을 받는다.

    Ref https://caniuse.com/css-matches-pseudo

    import React from “react”

    React 17부터 import React from "react"를 생략해도 되는 건 바벨7과의 협업 덕분이라고 한다.

    이전에는 컴포넌트가 반환하는 jsx를 React.createElement("div") 형태로 트랜스파일링 했기 때문에 상단에 React가 있어야 했지만, 트랜스파일링 옵션에 따라서 _jsx("div", {}, void 0);와 같은 형태로 반환해주면 상단에 React를 import하지 않아도 되는 원리다.

    Ref https://www.typescriptlang.org/docs/handbook/jsx.html#basic-usage

    reduce를 사용하여 Array의 원소들을 object의 key로 변경하기

    Copy
    const arr = ["name", "age", "country"];
    
    const obj = arr.reduce((accumulator, value) => {
      return { ...accumulator, [value]: "" };
    }, {});
    
    console.log(obj); // 👉️ {name: '', age: '', country: ''}

    Ref https://bobbyhadz.com/blog/javascript-convert-array-values-to-object-keys

    인터페이스의 모든 필드를 Nullable하게 만들기

    Copy
    type Nullable<T> = { [K in keyof T]: T[K] | null };

    Ref https://typeofnan.dev/making-every-object-property-nullable-in-typescript/


    이것저것

    • sql 쿼리의 실제 실행순서는 DBMS의 옵티마이저가 비용을 최소화한 실행계획에 따라 실행되어 생략되거나 순서가 바뀔 수 있다.

    • Stage 4에 올라와 있는 Object.fromEntries 메서드는 키값 쌍의 목록을 객체로 바꿔준다. 즉 entry로 객체를 다시 만들 수 있다. (Ref)

      Copy
      const entries = new Map([
        ["foo", "bar"],
        ["baz", 42],
      ]);
      
      const obj = Object.fromEntries(entries);
      
      console.log(obj);
      // expected output: Object { foo: "bar", baz: 42 }
    • 앱에서 특정 페이지를 띄울 때 remote config(서버를 통해 받아오는 동적 데이터)에 있는 url로 이동한다.

    • 그루비(Groovy)

      • 자바에 파이썬, 루비, 스몰토크등의 특징을 더한 동적 객체 지향 프로그래밍 언어다.
      • OKKY 서버가 그루비로 만들어져있다.
    • i18n에는 interpolation 기능이 있다. {{}} 안에 키값을 넣어서 다국어 대응을 더 편하게 할 수 있다. (Ref)

    • 스타일 내용을 담고 있는 style 변수명은 어떤 상황(lastDayOfMonth)을 빗대고 있는 이름을 가지기 보다는 leftGradient와 같이 스타일 친화적인 변수명을 사용하자

    • Verdaccio는 로컬에서 사용할 수 있는 Private NPM Registry이다. npm 패키지 배포를 테스트해보고 싶을 때 로컬에서 Verdaccio 서버를 띄워서 배포해볼 수 있어 유용하다.

    • node의 setInterval은 객체를 반환하고, window.setIntervalnumber를 반환한다.

    • 구동 환경에 따라 env에 우선순위가 있다. CRA 기준으로 한 우선순위는 아래와 같다.

      • npm start : .env.development.local, .env.development, .env.local, .env
      • npm run build : .env.production.local, .env.production, .env.local, .env
      • npm test : .env.test.local, .env.test, .env
    • \b는 backspace escape character로, 문자를 지우지 않고 커서만 뒤로 옮겼을 때 포함되는 문자다.

    • 제어 컴포넌트 - 입력 폼 엘리먼트는 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트한다. 리액트에서 state는 일반적으로 컴포넌트의 state 속성에 유지되고 setState()로 업데이트된다. 리액트의 state와 입력 엘리먼트의 value를 동일하게 통일시켜 React 컴포넌트에서 사용자 값을 제어할 수 있을 때, 이를 제어 컴포넌트라고 한다.

    • feature flag - 깃 플로우에서 개발 중인 기능을 지속적으로 통합하고 싶지만, 실제 환경에서는 보이지 않게 하거나 특정 사람에게만 보이도록 하고 싶을 때, 해당 기능과 관련된 feature flag를 코드 내에 설정하여 on/off해주는 방식으로 관리할 수 있다. 더 나아가 a/b test를 위한 발판도 쉽게 마련 가능하다.

    • git checkout --track {remote 브랜치명} - 로컬에서 remote에 있는 브랜치를 트래킹하는 브랜치를 만든다.

    • git checkout -t upstream/... - upstream에 있는 브랜치를 가져오기

    • package.json에서 peerDependencies 항목은 내가 만든 모듈이 다른 패키지에 직접 사용(require, import)되는 것은 아니지만 그 호스트 패키지와 호환성을 가지고 있는 것을 표현할 때 사용한다.

      • npm@3~npm@6 까지는 peerDependencies가 자동으로 설치되지 않고, dependencies에 누락된 경우 경고만 띄웠다. (dependencies 지옥에 빠지지 않도록 하기 위함이다.)
      • npm@7 부터는 자동으로 해결할 수 없는 dependencies 충돌이 없으면 peerDependencies가 자동으로 설치된다.
    • 젠킨스에서 브랜치명에 backup이 들어가면 안된다.

    • 개체는 객체에 포함되는 관계이다. 개체는 유일한 객체이다. 인스턴스 클래스로 비교를 하자면 instance는 개체, class는 객체라고 말할 수 있다.

    • clickable하지 않은 element에 click 이벤트를 넣는 것은 좋지 않다. 만약 하고 싶다면 cursor: pointer를 주자.

    • .tsbuildinfo - tsc가 컴파일할 때 참고하는 파일로, 컴파일 속도를 빠르게 하는 데에만 사용된다.

    • ‘명시도(Specificity)’는 브라우저가 특정 요소에 CSS 속성이나 스타일을 적용하기 위해서 사용하는 가중치(weight)다. 명시도가 높을수록 해당 선택자에 정의된 스타일이 가장 우선적으로 적용된다.


    기타

    프로그래머스 Dev Survey 2022

    프로그래머스에서는 생각보다 재밌는 걸 많이 하는 것 같다.

    개발자들이 가장 많이 하는 고민은 ‘전문성 부족’이었다. 끊임없이 성장을 외치는 개발자들은 정말… 피곤…하지만 나도 그래야 하는 걸…

    개발자라서 많이 듣는 말이 “내 컴퓨터 고쳐줘”에서 “나도 개발 배울까봐”로 바뀌었다고 한다. 나도 여러 번 들어본 말이다. 주변 사람들에게 그래 한번 해보라고 말해준다.

    책상 아이템에 이산화탄소 측정기는 왜 갖고 있는지 모르겠다. 순위권에 스탠딩 데스크가 있어서 괜시리 뿌듯.

    Ref https://programmers.co.kr/pages/2022-dev-survey

    React 18 한국어 번역본

    아 내가 먼저 쓸 걸.

    Ref https://medium.com/@yujso66/%EB%B2%88%EC%97%AD-react-v18-0-9da9a6b838bd

    비동기 코드를 짤 때 도움이 되는 린트 툴

    Promise에서 catch를 사용하지 않는다거나, Error 객체로 감싸지 않은 에러 메시지를 던진다거나 하는 등 사소하게 코드의 퀄리티를 떨어뜨리는 경우들을 사전에 잡아주는 린트 툴들이 있다. 또 콜백 헬을 잡아주고, 불필요한 await 키워드를 남용하지 않게 해주는 린트들도 유용할 것 같다.

    Ref https://maximorlov.com/linting-rules-for-asynchronous-code-in-javascript/


    마무리

    평일에는 정말정말 바빴다. 거의 야근 풀타임으로 채우지 않았을까 🤦‍♀️ 혼자 일하는 게 아니라, 기획-디자인-백엔드 팀원 분들과 함께 일하며 마주하는 여러 상황들, 특히 예외 케이스들에 대해서 미리미리 파악하고 말씀드려야겠다는 다짐을 해본다.

    주말이 되며 완연한 봄날씨가 돼버렸다. 벚꽃은 만개했고, 주말 내내 나가며 봄을 만끽중이다. 분명 며칠 전까지는 그래도 밤엔 추웠는데, 이제 가장 더운 시간엔 땀까지 난다. 다이나믹 코리아의 여름이 벌써 걱정된다. 🌻


    Relative Posts:

    4월 3주차 기록

    April 15, 2022

    4월 첫주차 기록

    April 1, 2022

    zigsong

    지그의 개발 블로그

    RotateLinkImg-iconRotateLinkImg-iconRotateLinkImg-icon