녕후킴

11월 3주차

0 views

📌 Union Find

아래 코드의 union 로직은 time limit exceeded가 발생할 확률이 크다.

시간 복잡도에 있어서 병목이 발생할 수 있는 부분은 union된 노드가 만드는 트리가 Imbalanced 돼있을 때이다. 즉, 리스트처럼 0 - 1 - 2 - 3 - 4 - 5로 연결돼서, tree의 height이 노드의 개수와 같아지면 트리는 매우 Imbalanced한 상태로 볼수있다. 0 - 1 - 2 - 3 - 4 - 5와 같이 union 돼있을 때, this.find(5)가 실행되면, height만큼 탐색을 해야한다.

이를 해결하기 위해서, 아래와 같이 특정 root 노드가 얼마나 많은 노드들과 결합되어 있는지를 알수 있는 count 배열을 만든다. 그리고 2개 이상의 노드가 존재하는 트리를 union 해야할 때 노드가 적은 트리를 노드가 많은 트리에 결합하는 규칙을 적용한다.

이유는 아래 그림처럼 노드가 많은 트리를 노드가 적은 트리에 결합하는 경우, height이 1개 더 증가하기 때문이다. Union Find Algorithm: Brief but Comprehensive Guide

union find 3


📌 next/image 컴포넌트 width 100% 만들기

next/image(이하 이미지 컴포넌트)는 CLS 방지를 위해서 width와 height을 기입해야한다. 만약 어떤 이미지 컴포넌트를 부모 컴포넌트 내에 꽉 차게 만들고 싶다면, layout은 fill prop을 전달하면 이미지가 부모 컴포넌트에 꽉 차게된다.

단, 부모 컴포넌트 역시도 width와 height을 기입해주어야 한다. 우리가 width 100%를 적용하고 height이 그에 따라서 늘어나길 기대할 수 없다는 말이다. 이러한 next/image 성질에 대해서 이미 여러 개발자들이 불편을 토로해 왔다.

이를 해결하기 위해서는 Image 컴포넌트를 감싸는 Wrapper 컴포넌트에 다음과 같은 스타일을 적용한다.

  '& > span': {
    position: 'unset !important'
  },

  '& > span > img': {
    objectFit: 'contain',
    width: '100% !important',
    position: 'relative !important',
    height: 'unset !important'
  }

Wrapper 컴포넌트의 경우 여러 태그들이 올수 있기 때문에(div, button 등), template 태그로 컴포넌트를 만들었고, 사용하는 쪽에서 as 프로퍼티를 통해서 태그를 수정하여 사용했다.

// ./baseComponents.js
export const NextImageWrapperTemplate = styled('template')(() => ({
  '& > span': {
    position: 'unset !important',
  },

  '& > span > img': {
    objectFit: 'contain',
    width: '100% !important',
    position: 'relative !important',
    height: 'unset !important',
  },
}))

// index.js
import NextImageWrapperTemplate from './baseComponents'
import Image from 'next/image'

const ComponentFunction = () => {
  return (
    <NextImageWrapperTemplate as="button">
      <Image />
    </NextImageWrapperTemplate>
  )
}

📌 Norminal Typing, Structural Typing, Duck Typing

이 글은 Structural, Nominal, and Duck typing을 오,의역 했다.

Norminal Typing, Structural Typing, Duck Typing은 타입 시스템이 타입 호환성을 비교하는 서로 다른 방법들이다.

주어진 두개의 타입이 (1) 서로 호환되거나 (2) 하나의 타입이 다른 타입의 부모 타입인 경우 타입 호환성을 얻을 수 있다.

타입 호환성 = 두개의 타입이 서로 호환 || 하나의 타입이 다른 타입의 부모 타입

T1 타입이 T2 타입과 호환 가능하다고 할때, T2 타입이 T1 타입과 호환 가능함을 의미하지는 않는다. 예를들면, Integer 데이터를 Decimal 데이터로 바꾸는 것은 가능하지만, Decimal 데이터를 Integer 데이터로 바꾸는 것은 불가능하다. (Decimal 타입이 무엇인지 이해하려면 깊게 들어가야 하므로, 여기서는 단순히 Decimal이 Integer의 자식 타입 정도로만 이해하고 넘어가자.)

Norminal Typing은 타입 호환성을 비교하기 위해서 ‘이름’을 사용하고, Structural Typing은 타입 호환성을 비교하기 위해서 ‘구조’를 사용한다. Duck Typing은 사용에 기반을 둔 ‘구조’를 사용하며(당장 이해가 어렵지만 뒤에 예제를 보면 무슨 말인지 이해가 될 것이다.), 동적 타입 언어에서 보통 발견할 수 있다.

1. Norminal Typing

Norminal Typing을 사용하는 타입 시스템에서는 타입 호환성을 위해서 다음 두가지중 하나가 만족되어야 한다.

  1. 타입 이름이 매칭되어야 함 (동일한 클래스로부터 만들어진 객체들이라고 표현하는게 이해가 더 빠른 것 같다. 🤔)
  2. 하나의 타입이 다른 타입의 부모 타입이어야 함 (상속 관계에 있는 클래스로부터 만들어진 객체들이라고 표현하는게 이해가 더 빠른 것 같다. 🤔)

먼저 1의 상황에 대해서 이야기해보자. 아래는 C# 예제다. Dog와 Cat 클래스가 name 프로퍼티와 makeNoise 메서드를 가지고 있다고 가정하자. 그리고 다음과 같이 객체를 생성하고,

Dog cat = new Dog('Mars');
Cat cat = new Cat('Venus');

다음 메서드에 위 두가지 객체를 각각 전달한다고 할때,

static public void makeNoise(Dog obj) {
  obj.makeNoise();
}

Dog 인스턴스에 대해선 동작을 하지만 Cat 인스턴스에 대해서는 동작하지 않는다. 설령 두 인스턴스의 모양이 동일하더라도, C#은 둘을 호환 가능하다고 취급하지 않는 것이다.

이렇듯 Nominal Typing은 서로 다른 타입으로부터 생성된 객체는 호환이 불가능하다. 앞서 2에서 이야기한 상속 관계가 아니라면 말이다.

2. Norminal Typing with Subtypes

A를 상속받는 B가 존재한다고 할때, B 를 A 로 호환하는 것은 가능하지만, A 를 B 로 호환하는 것은 불가능하다.

앞선 예제에 이어서, 다음과 같이 BullDog이 Dog를 상속받는다고 가정해보자.

public class BullDog:Dog {
  public String breedName;
  public BullDog(string name, string breedName) : base(name) {
    this.breedName=breedName;
  }
}

이제 BullDog 타입은 Dog의 대체 타입으로 사용 가능하기 때문에, makeNoise 메서드의 인자로 전달할 수 있게된다.

static public void makeNoise(BullDog obj) {
     obj.makeNoise();
  }

만약 다음과 같이 makeNoise의 파라미터 타입을 Dog로 바꾼다면, BullDog 인자를 전달하면 동작하지 않게된다.

Dog dog = new Dog("Mars");
makeNoise(dog);

//error CS1503: Argument 1: cannot convert from 'Dog' to 'BullDog'

3. Structural Typing

Structural Typing에서는 타입의 이름보다 타입의 형태가 더 중요하다. 그리고 Typescript는 Structural Typing 시스템이다. 앞선 예제를 Typescript로 옮겨보자.

Dog와 Cat 클래스 모두 다음과 같이 name 프로퍼티와 makeNoise 메서드가 존재한다고 할때,

class Dog {
  constructor(public name: string) {}
  makeNoise() {
    console.log('Woof')
  }
}

class Cat {
  constructor(public name: string) {}
  makeNoise() {
    console.log('Miau')
  }
}

makeNoise 함수의 인자로 두개 모두 전달이 가능하다.

let dog = new Dog('Mars')
let cat = new Cat('Venus')

function makeNoise(obj: Dog) {
  obj.makeNoise()
}

makeNoise(dog) //Ok
makeNoise(cat) //Ok

만약 Person 클래스가 makeNoise 메서드를 갖고 있고, name 프로퍼티를 갖고 있지 않다면, 앞선 makeNoise 함수에 전달이 불가능하게 된다.

class Person {
  constructor(public firstName: string, public lastName:string) {
  }

  makeNoise() {
    console.log('Helloooo')
  }
}

let person= new Person("Jon","Snow")

makeNoise(person) //Error
//Argument of type 'Person' is not assignable to parameter of type 'Dog'.
//  Property 'name' is missing in type 'Person' but required in type 'Dog'.

만약 Person 클래스에 name 프로퍼티가 있고, 더불어서 address 프로퍼티가 존재해도 makeNoise의 인자로 전달이 가능하다.

class Person {
  constructor(public name:string,public address: string) {
  }

  makeNoise() {
    console.log('Helloooo')
  }
}

let person= new Person("Jon Snow","North")

makeNoise(person)

4. Duck Typing

Duck Typing에서 이름과 형태는 중요하지 않다. Duck Typing은 어떤 동작을 위해 필요한 메서드와 프로퍼티가 존재해야 하며, 보통 javascript와 같은 동적 타입 언어에서 찾아볼 수 있다.

“어떤 것이 오리처럼 걷고, 오리처럼 수영하고, 오리처럼 꽥꽥거리면 그것은 아마도 오리일 것이다”라는 것에서 Duck Typing이 유래했다. 만약 어떤 객체가 어떤 행동을 하길 기대하고, 그 객체가 그 행동을 한다면 개발자는 이 객체를 사용할 수 있다. 이 객체의 모양과 타입은 전혀 중요하지않다.

아래는 person과 backAccount 두가지 객체를 포함하고 있는 자바스크립트 예제이다. 그들은 동일한 타입을 공유하지도 않고, 동일한 구조를 갖고있지도 않다. 다만 동일하게 someFn 메서드를 갖고 있을 때, invokeSomeFn이라는 someFn 메서드를 호출하는 함수의 인자로 각각의 객체를 문제 없이 전달할 수 있다.

let person = {
  name: 'Jon',
  someFn: function () {
    console.log('hello ' + this.name + ' king in the north')
  },
}

let bankAccount = {
  accountNo: '100',
  someFn: function () {
    console.log('Please deppost some money')
  },
}

invokeSomeFn = function (obj) {
  obj.someFn()
}

invokeSomeFn(person)
invokeSomeFn(bankAccount)

5. Norminal vs Structural vs Duck

Norminal Typing이 가장 덜 유연하고, Duck Typing이 가장 유연하며, Structural Typing은 그 사이에 있습니다.

타입이 덜 유연할수록 타입을 명시적으로든 암시적으로든 지정해주어야하기 때문에, 더 많은 코드가 요구되고 덜 유연할 수 있지만, 가독성이 좋아지고, 에러가 덜 발생하며, 버그를 쉽게 찾아 수정할 수 있습니다.


📌 Lighthouse 측정 관련

web vital 측정을 위해 Lighthouse를 사용했는데, 아래 사진과 같이(당시에는 아래 사진보다 더 심하게 측정값이 달라졌었다.) 측정할 때마다 METRIC이 크게 달라지는 문제를 만났었다. 이렇게되면 성능 개선에 대한 Before와 After 측정이 어려웠는데, 다른 팀원들에게 물어보니 나처럼 결과값이 크게 변동하는 사람이 없었다.

어디서 이런 차이점이 발생할까하고 곰곰히 생각해보다가, lighthouse는 제어되는 환경에서 테스트를 진행한다고 햇는데, 내가 상황을 제어하여 테스트했는지에 대한 의문이 들었다.

그래서 lighthouse 측정을 실행하고 탭을 이리저리 바꾸거나(측정 대상이 되는 탭을 background로 돌림), 새로운 윈도우를 하나 띄어서 여러 탭을 만들고 유튜브 동영상에 들어가는 등 부가적인 작업을 돌리니 결과값이 달라지는 것을 확인했다.

어찌보면 너무나도 중요하고 당연한 부분인데 간과했던 것 같다. 😥