8월 2주차

2023년 08월 13일, 07:13

Visitor 디자인 패턴

방문자 패턴은 알고리즘들이 그들이 작동하는 객체로부터 분리할 수 있도록 하는 행동 디자인 패턴이다. 이렇게하면 기존의 구조를 건드리지 않고 새로운 로직을 추가할 수 있다.

예제

문제

하나의 거대한 그래프로 구성된 지리 정보를 사용해 작동하는 앱을 개발한다고 가정해보자. 그래프의 각 노드는 도시와 같은 복잡한 객체를 나타낼 수 있지만, 산업, 관광 지역 등의 더 세부적인 항목도 나타낼 수 있다.

만약 노드가 나타내는 실제 객체들 사이에 도로가 있으면 노드는 연결된다.

이제 이 그래프를 XML로 내보내는 작업의 구현을 하게 되었다. 하지만, 기존 노드 클래스를 변경하는 것은 허용하지 않아야한다. (실제 프로덕션 코드에 나가 있어서 기존 코드의 변경이 일어나지 않았으면 하는 경우)

또, 노드 클래스 내에서 XML내보내기 코드를 넣는 것이 적절한지에 대한 의문.

노드 클래스의 주 작업은 지리 데이터를 처리하는 것이지 XML 내보내기 동작은 지리 데이터를 처리하는 것이라고 보기 어렵다.

해결책

새로운 구현사항을 기존의 클래스에 추가하는 것 대신 visitor라는 별도의 클래스를 만들면 된다. XML 내보내기 로직을 visitor가 가지고 지리 데이터를 가지고 있는 객체를 메소드의 인자로 받아서 실행된다.

class ExportVisitor implements Visitor {
  doForCity(c: City) {
    // ...
  }

  doForIndustry(f: Industry) {
    //...
  }

  doForSightSeeing(c: SightSeeing) {
    //...
  }
}

위와 같은 경우 메서드를 각 노드의 클래스에 맞게 호출해야한다.

for(const node of nodes){
  if(node instanceof City){
    exportVisitor.doForCity(node)
  }

  if(node instanceof Industry){
    exportVisitor.doForCity(node)
  }

  if(node instanceof SightSeeing){
    exportVisitor.doForCity(node)
  }
}

타입스크립트에서는 다형성을 사용할 수도 없고, 해당 코드는 시그니처도 달라서 사용할 수 없다. 그리고 노드의 수가 많아진다고 생각해보면 코드가 복잡해진다.

그래서 비지터 패턴에서는 더블 디스패치라는 방법을 사용하여 해결한다.

이 방법은 번거로운 조건문 없이 객체에 맞는 메서드를 실행하게 도와준다. 클라이언트가 호출할 메서드의 적절한 버전을 선택하도록 하는 대신 이 선택권을 비지터에게 인수로 전달되는 객체로 위임한다.

이렇게하면 객체들은 자신들의 클래스를 알고 있기 때문에 비지터의 자신의 클래스에 맞는 메서드를 실행할 수 있다.

for (const node of nodes) {
  node.accept(exportVisitor);
}

class City extends Node1 {
  accept(v: Visitor) {
    v.doForCity(this);
  }
}
class Industry extends Node1 {
  accept(v: Visitor) {
    v.doForIndustry(this);
  }
}
class SightSeeing extends Node1 {
  accept(v: Visitor) {
    v.doForSightSeeing(this);
  }
}

결국 노드 클래스를 변경했지만, 변경 사항은 최소한으로 했고 비지터는 복잡한 조건문으로 노드 클래스를 찾지 않아도 된다.

이제 만약 새로운 Visitor를 만들고 싶다면 기존 코드의 변화 없이 Visitor 추상 클래스의 구현을 만족하는 구체 클래스를 만들어서 새로운 로직을 기존 코드에 적용할 수 있다.

장점

  • 개방/폐쇄의 원칙
  • 단일 책임의 원칙
  • 객체 트리와 같은 복잡한 객체 구조를 순회하여 각 객체에 비지터 패턴을 적용하는 경우 유용

단점

  • 클래스가 요소 계층구조에 추가되거나 제거될 때마다 모든 비지터를 업데이트 해야함
  • 비지터들은 함께 작업해야하는 요소들의 비공개 필드 및 메서드들에 접근하기 위해 필요한 권한이 부족

참고)


휴리스틱

불충분한 시간이나 정보로 인하여 합리적인 판단을 할 수 없거나, 체계적이면서 합리적인 판단이 굳이 필요하지 않은 상황에서 사람들이 빠르게 사용할 수 있게 보다 용이하게 구성된 간편추론의 방법 - 위키백과

 가능한 가장 좋은 해답 혹은 최적의 해결법에 접근하기 위한 빠른 방법을 얻기 위해 특히 쓰인다.

휴리스틱 알고리즘 기본적으로 모두 최적해가 될 가능성이 없는 답들을 탐색하는 것을 방지하여 만들어 봐야 할 답의 수를 줄이는 것을 목표로 한다.

react에서도 가상 돔의 diff를 알기 위해서 사용하고 있는 것으로 알고 있다.

  • 가지치기(pruning) 기법
  • Simulated Annealing (담금질 기법)
  • Genetic Algorithms (유전 알고리즘)

참고)


react-query의 useQuery callback 함수의 deprecated

v5 부터는 useQuery에서 onSuccess, onError 등의 콜백 함수가 사라진다.

예상대로 동작하지 않을 가능성이 높은 API이기 때문인데, API에서 가장 중요한 것은 일관성인데 useQuery의 콜백은 그렇지 않다.

예제로 살펴보면,

나쁜 API
export function useTodos() {
  return useQuery({
    queryKey: ['todos', 'list'],
    queryFn: fetchTodos,
    onError: (error) => {
      toast.error(error.message)
    },
  })
}

쿼리가 실패하는 경우 콜백을 사용해서 에러 알림 같은 effect를 생성할 수 있다. 이 API는 직관적으로 보인다. 이렇게 하지 않는 경우 useEffect를 이용해서 처리해줘야한다.

하지만 이 경우에 에러가 있는데, useTodos를 2번 실행하는 경우 에러 알림이 두 번 노출된다.

useEffect를 사용하는 경우에는 각 컴포넌트에서 두 번 중복으로 호출 되는 것을 잘 볼 수 있는데, useQuery에 콜백으로 넘기는 경우 그렇지 않다.

상태 동기화

콜백이 잠재적으로 여러번 실행될 수 있는 것 외에 사람들이 콜백을 사용해 상태를 동기화 하는 경우도 있다.

export function useTodos() {
  const [todoCount, setTodoCount] = React.useState(0)
  const { data: todos } = useQuery({
    queryKey: ['todos', 'list'],
    queryFn: fetchTodos,
    //😭 제발 이러지 마세요
    onSuccess: (data) => {
      setTodoCount(data.length)
    },
  })

  return { todos, todoCount }
}

추가적인 렌더링의 발생

setTodoCount를 사용하게 되면 다른 렌더링 사이클을 끼워 넣는 셈이다.

이건 앱을 필요 이상으로 자주 렌더링 되게 만들 뿐만 아니라(문제되거나 안될 수도 있겠지만)

중간의 렌더링 사이클에 잘못된 값이 포함될 수 있다.

ex) 

fetchTodos는 길이가 5인 목록을 반환한다고 가정한다면 위 코드에서 렌더링 사이클은 세 번입니다.

  1. todos는 undefined이고 길이가 0. Query가 fetch되는 동안의 초기 상태이며 올바른 상태
  2. todos는 길이가 5인 배열이 되고 todoCount는 0이 됨. 이건 useQuery와 onSuccess는 이미 실행을 마쳤고 setTodoCount는 예약된 중간의 렌더링 사이클입니다. 값들이 동기화되지 않았기 때문에 잘못된 상태로 존재.
  3. todos는 길이가 5인 배열이 되고 todoCount는 5가 됩니다. 이게 최종 상태이며 다시 올바른 상태

만약 동기화 되지 않는 데이터로 렌더링 된다면 문제가 분명히 있을 것이다. 추가로 동기화되지 않은 상태로 무언가 로직이 처리된다면 이 또한 문제이다.

간단한 해결책은 상태를 파생시키는 것이다.

export function useTodos() {
  const { data: todos } = useQuery({
    queryKey: ['todos', 'list'],
    queryFn: fetchTodos,
  })

  const todoCount = todos?.length ?? 0

  return { todos, todoCount }
}

위에서 살펴본 예제같이 여러가지 문제점을 가지고 있는 API이기 때문에 v5부터는 삭제된다.(더 많은 예제는 블로그 참고.)

버저닝에 대한 뼈아픈 일침(?)이 있어 적어본다.

완전 동의합니다. semver의 메이저 버전 숫자는 새로운 걸 의미하는 게 아니라 API를 몇번이나 잘못 만든 건지 영원히 상기시켜주는 겁니다. semver는 메이저.마이너.패치가 아니라 실패.기능.버그를 의미합니다.

메이저 버전 업데이트는 기존의 설계의 실패라고 생각할 수 있겠다..

참고)


CSS에서의 env funtion

var function이랑 비슷하다. var는 유저가 세팅하는 값이라면, env는 사용자의 user-agent에 따라서 정의된 값을 사용할 수 있다.

사용하는 곳은 아이폰 노치와 같은 것을 대응할 때 사용한다.

/* Using the four safe area inset values with no fallback values */
env(safe-area-inset-top);
env(safe-area-inset-right);
env(safe-area-inset-bottom);
env(safe-area-inset-left);

/* Using them with fallback values */
env(safe-area-inset-top, 20px);
env(safe-area-inset-right, 1em);
env(safe-area-inset-bottom, 0.5vh);
env(safe-area-inset-left, 1.4rem);

이 외에도 아래와 같은 값이 있다.

  • titlebar-area-x : PWA를 사용하는 경우 데스크탑에서 window control 버튼에 겹치지 않게 도와준다.
  • keyboard-inset-x : VirtualKeyboard의 위치나 크기에 대한 정보를 제공한다.

참고)


기타

RFC

Request for Comments '의견을 요청하는 문서'라는 의미로 국제 인터넷 표준화 기구(IETF; Internet Engineering Task Fource)에서 관리하는 기술 표준

승인된 문서는 유일한 일련 번호를 갖게 되며 'RFC-일련번호' 형식

참고)

spellCheck

전역 속성으로 스펠링을 체크할 것인지 아닌지 판단하는 속성이다.

![[스크린샷 2023-08-11 오전 6.17.36.png]]

contenteditable

전역속성으로 사용자가 요소를 편집할 수 있는지 나타내는 특성이다.

위의 예제 같이 참고

참고)

styled-components의 pass props

$ 를 styled-components props로 전달해주면 해당 props는 태그의 attributes로 전달되지 않는다.

예제)

// Create an Input component that'll render an <input> tag with some styles
const Input = styled.input<{ $inputColor?: string; }>`
  padding: 0.5em;
  margin: 0.5em;
  color: ${props => props.$inputColor || "#BF4F74"};
  background: papayawhip;
  border: none;
  border-radius: 3px;
`;

// Render a styled text input with the standard input color, and one with a custom input color
render(
  <div>
    <Input defaultValue="@probablyup" type="text" />
    <Input defaultValue="@geelen" type="text" $inputColor="rebeccapurple" />
  </div>
);

위의 코드 실행 결과 styled-component-pass-props

참고)