[TIL] 2022-04-28 / 28일차

미음제

·

2022. 4. 29. 19:05

 

오늘 배운 내용

 

오늘도 어김없이 하루 지나 작성하는 TIL. 과제를 다 끝내고 PR 날린 후에 작성하려 했는데, 어쩌다 보니 과제를 시간에 빡빡하게 맞춰 제출하게 되어 하루 지나 TIL을 작성하게 되었다.

 

URL() 생성자

 

지금 구현된 코드에서는 state에 대한 정합성 체크를 전혀 하지 않는데, 이 부분을 보충해주세요.

 

고양이 사진첩 만들기 강의를 듣고, 해당 코드에서 위와 같은 요구사항을 추가해 과제를 진행하는 것이 있었다. 필요한 state들이 있는지, state들의 값이 올바르게 들어가 있는지 체크하는 내용이다. 그래서 현재 프로젝트에서 어떤 state를 사용하는지 확인한 후, state 정합성 체크를 진행했다.

 

  • App.js : [ isRoot, isLoading, nodes, paths, selectedUrl]

 

최상위 컴포넌트 App.js에 들어가는 state들이다. isRoot는 boolean 형태, isLoading은 boolean 형태, paths는 배열 형태, selectedUrl은 image 주소를 담고 있는 string 형태이다.

 

  • Loading.js : [ isLoading]
  • Nodes.js : [ isRoot, nodes, selectedUrl]
  • ImageViewer.js : [ selectedUrl ]
  • Breadcrumb.js : [ paths ]

 위 4개 파일은 App.js에 속해있는 하위 컴포넌트들이다. 각 컴포넌트들의 state는 최상위 컴포넌트 App.js에서 내려주는 state들을 사용하기 때문에, 컴포넌트 별로 정합성 체크는 따로 하지 않았다.

 

컴포넌트별로 올바르지 않은 state를 넣으면 오류가 발생하도록 해주세요.

 

따로 하지 않은 이유는 최상위 컴포넌트에서 하위 컴포넌트로 state를 내려주기 때문에 최상위에서 검증을 하면 굳이 또 검증을 하지 않아도 될까 하는 생각에 진행하지 않았다. 하위 컴포넌트의 initialState는 하드 코딩식으로 작성되어 있기 때문에 정합성 검증이 필요 없다고 생각되었고, state의 변경이 일어날 때 검증이 필요한데, App의 state가 변경되면 변경된 state가 하위 컴포넌트의 setState로 뿌려지는 구조라서 진행하지 않았다. 이 부분은 PR의 피드백 사항으로 질문을 했기에 답변을 얻고 어느 게 맞는지 확인할 예정이다.

 

다시 돌아와서, isRoot, isLoading은 true or false의 boolean 형태만 확인해주면 되고, paths, nodes는 경로와 node가 담긴 배열인지 확인해주면 되었다. 문제는 string 형태의 image 주소가 담긴 selectedUrl이었는데, 단순 string 형태인지만 검사하기에는 올바르지 않은 문자열이 들어가도(이미지 주소가 아닌 다른 문자열) 정합성 검사를 통과할 것 같아서 고민했다.

 

생각해낸 해결 방안

 

  1. 정규표현식
  2. URL() 생성자

 

1번의 방식이 제일 오리지널(?) 방식이라고 생각된다. 들어온 문자열 값을 정규표현식을 활용해 url 주소가 올바른지 체크하는 방식이다. 정규표현식에 약하기 때문에(공부 해야지 해야지 하는데 몇 년째 미루고 있다), 다른 방법을 찾아봤고, 우연히 알게 된 URL() 생성자를 활용했다.

 

 

URL() - Web API | MDN

URL() 생성자는 매개변수로 제공한 URL을 나타내는 새로운 URL 객체를 반환합니다.

developer.mozilla.org

 

URL() 생성자를 통해 URL 객체를 생성할 수 있다. 매개변수로 string 형태의 url 주소를 넘겨주면, URL 객체를 반환해준다. 이 때, 넘겨주는 url 주소는 'http://', 'https://' 형식의 url 주소를 넘겨주어야 한다.

 

function isValidUrl(string) {
  try {
    new URL(string);
  } catch (_) {
    return false;
  }

  return true;
}

console.log(isValidUrl('www.naver.com')) // false
console.log(isValidUrl('https://www.naver.com')) // true

 

'http' 혹은 'https'가 없는 경우 false를 return한다. 정확하게 입력한 경우에만 true를 return 한다.

 

이 점을 활용해 과제의 정합성 체크에 추가했다.

 

const STATE_KEY_AND_VALUE = {
  isRoot: 'Boolean Type',
  isLoading: 'Boolean Type',
  nodes: 'Array Type',
  paths: 'Array Type',
  selectedImageUrl: 'Image Path',
};

const isValidUrl = (urlString) => {
  try {
    new URL(urlString);
  } catch (_) {
    return false;
  }
  return true;
};

export default function CheckState(state) {
  Object.entries(state).map((item) => {
    const [key, value] = item;
    if (key in STATE_KEY_AND_VALUE) {
      // 생략
      if (STATE_KEY_AND_VALUE[key] === 'Image Path') {
        if (value === undefined) {
          return true;
        }
        if (!isValidUrl(value)) {
          alert(Error(`${key} state 에러`));
          throw new Error(`${key} state 에러`);
        }
      }
    } else {
      throw new Error(`${key} State가 없습니다.`);
    }
    return true;
  });

  return true;
}

 

imagePaths를 검증할 때, isValidUrl 함수로 string 형태의 url을 넘겨주고, URL() 생성자를 통해 true를 return 하는지 false를 return 하는지 확인하는 구조다. image에 대한 API 요청 응답으로 'https://이미지 주소'를 보내주기 때문에 활용했던 것이다.

 

단순하게 들어온 형태가 string이라 해서 string인지만 확인하는 것보다는 url의 형태의 string인지 검사하기 위함이었다. 그러나 이 상태로 "https://ww.naver.com"을 검사하면 true가 반환된다. 단지 https나 http로 시작만 하면 true를 반환해주는 듯하다. 이 부분에 대한 방어 코드가 작성이 필요해 보이는데, API 요청 응답으로 https://올바른 주소. 형태로 내려오기 때문에 이 부분에 대한 방어 코드가 필요한지는 의문이 든다. 

 

let a = new URL("http://www.naver.com");
console.log(a);

/*
URL {
  href: 'http://www.naver.com/',
  origin: 'http://www.naver.com',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'www.naver.com',
  hostname: 'www.naver.com',
  port: '',
  pathname: '/',
  search: '',
  searchParams: URLSearchParams {},
  hash: ''
}
*/

 

URL 객체를 생성하면 위와 같이 확인할 수 있다. port, hostName, host, path, origin 등 다양한 것을 확인 할 수 있는데, 방어 코드를 작성한다면 path를 따로 불러오고 .jpge나 .png 형태의 문자열이 있는지 검사하는 형태로 진행할 듯싶다.

 

 

 

객체 비교

 

각 컴포넌트의 setState를 최적화하여 이전 상태와 비교해서 변경사항이 있을 때만 render 함수를 호출하도록 최적화를 해봅니다.

 

다른 요구사항으로 위와 같은 요구사항이 있었다. 최상위 App 컴포넌트에서 하위 컴포넌트로 setState가 일어날 때마다 하위 setState를 불러오고 있어 계속해서 렌더링이 발생했다. 이 문제를 해결하기 위함이었다.

 

state의 형태가 객체 형태이기 때문에 이 부분에 대해 고민했었다. 우선 처음 고민한 것은 객체 자체를 하나하나 비교하는 것이었다. 해당 객체의 key, value를 가져와 이전 객체와 현재 객체가 다른지 확인하는 방향으로 진행했다.

 

export const CompareBreadcrumbState = (previousState, nextState) => {
  const previousStateKey = Object.keys(previousState);
  const nextStateKey = Object.keys(nextState);

  if (previousStateKey.length === nextStateKey.length) return false;

  for (const [key, value] of Object.entries(previousState)) {
    if (value !== nextState[key]) {
      return true;
    }
  }

  return true;
};

 

현재 경로를 나타내 주는 Breadcrumb에 대한 state 비교이다. 이전 state와 다음 state를 비교하는 것인데, 우선 keys의 길이를 통해 비교했다. keys의 길이가 같으면 경로가 바뀌지 않았다고 생각해 false를 return 하도록 했고, 길이가 다른 경우, 쌓여 있는 경로(value)를 비교해 다른 값이 들어왔다면 true를 return 하도록 했다.

 

우선 이렇게 하나의 컴포넌트를 비교하고 다음 컴포넌트들을 비교하고 나니 코드가 일관성도 없어 보이고, 복잡한 느낌이 들었다.

 

export const CompareBreadcrumbState = (previousState, nextState) => {
  const previousStateKey = Object.keys(previousState);
  const nextStateKey = Object.keys(nextState);

  if (previousStateKey.length === nextStateKey.length) return false;

  // eslint-disable-next-line no-restricted-syntax
  for (const [key, value] of Object.entries(previousState)) {
    if (value !== nextState[key]) {
      return true;
    }
  }

  return true;
};

export const CompareIsLoadingState = (previousState, nextState) => {
  if (previousState === nextState) return false;
  return true;
};

export const CompareNodeState = (previousState, nextState) => {
  const previousIsRoot = previousState.isRoot;
  const nextIsRoot = nextState.isRoot;
  const previousNodes = previousState.nodes;
  const nextNodes = nextState.nodes;

  if (previousIsRoot !== nextIsRoot) {
    return true;
  }
  if (previousNodes !== nextNodes) {
    return true;
  }
  return false;
};

export const CompareImageViewer = (previousState, nextState) => {
  if (previousState.selectedImageUrl !== nextState.selectedImageUrl) return true;
  return false;
};

export default {
  CompareBreadcrumbState,
  CompareIsLoadingState,
  CompareNodeState,
  CompareImageViewer,
};

 

4개 컴포넌트를 비교하는 것인데, 굉장히 비효율적이라고 생각했다(실제 효율성보다는 코드를 작성하는 게 너무 귀찮고 번거로웠다. 각 컴포넌트별로 state의 형태가 다르니까 함수를 매번 생성해야 했다).

 

그러다 문득 든 생각이 JSON.stringify()를 통해 객체를 비교하면 되지 않을까 생각이 들었다. 이전 state와 다음 state를 둘 다 JSON.stringify()를 통해 문자열로 바꾸고 비교를 하고, 다른 경우에만 render()를 호출할 수 있도록 state 변경을 해주면 되지 않을까 싶었다.

 

팀 스크럼에서 질문을 해보니, 문자열이다 보니 객체를 stringify()를 하기 전에 정렬을 해주고 비교를 해야 하지 않을까 라는 의견을 들었다.

 

let obj1 = {
  name: 'MJ',
  age: 27,
};

let obj2 = {
  name: 'MJ',
  age: 27,
};

console.log(JSON.stringify(obj1) === JSON.stringify(obj2)) // true

 

대충 이전 state를 obj1이라고 하고 다음 state를 obj2라고 했을 때, 이런 상태는 true가 return 된다. 

 

let obj1 = {
  name: 'MJ',
  age: 27,
};

let obj2 = {
  age: 27,
  name: 'MJ',
};

console.log(JSON.stringify(obj1) === JSON.stringify(obj2)) // false

 

그러나 이렇게 순서를 바꾸면 false가 return 된다. 실제 상태의 변경은 없지만 순서가 달라 문자열로 비교하니 다르게 나오는 것이다. 여기서 든 생각은 "setState를 할 때, state 객체의 순서가 변경될 가능성이 있는가?"다. 개인적인 생각으로는 상관없어서 stringify()를 통해 비교하는 게 훨씬 효율적으로 보인다(stringify()로 변환하고 비교하는 조건문만 넣어주면 모든 컴포넌트의 state를 비교할 수 있으니까).

 

이 부분에 대해서도 PR에 날려 질문을 드렸으니, 추후에 확인해보아야겠다.

 

부족한 점 & 느낀 점

 

어제 과제를 제출하고 어제 TIL을 작성하려 했는데, 시간 분배 실패로 오늘 작성했다. 오늘 TIL을 작성하면서 느낀 것인데, 지금 제출한 코드를 다시 보니까 고쳐야 할 부분들이 많이 보인다. URL() 생성자를 통해 비교하는 것만 봐도 그렇고, 전체적으로 손 볼 곳들이 보인다.. 이러고 피드백 반영 기간이 되면 또 까먹을 수 있으니까 이 부분에 대해 메모해 두고 피드백 반영 때 손봐야겠다. 과제 제출을 마감일에 맞춰 하기보단 하루에서 이틀 정도 시간을 남겨 제출할 수 있도록 해야겠다. 스스로 짠 코드를 시간이 지나고 다시 보면 그땐 안보였던 게 보이는 것 같다.

 

 

 

 

반응형