상권's

TIL 14 (2021.10.20) 본문

~2022 작성 글/TIL

TIL 14 (2021.10.20)

라마치 2021. 10. 20. 16:06
-2021.10.20 오늘의 코플릿-
오름차순 정렬된 정수의 배열(arr)과 정수(target)를 입력받아 target의 인덱스를 리턴해야 합니다.
이진탐색 알고리즘(O(logN))을 사용해야 합니다.
단순한 배열 순회(O(N))로는 통과할 수 없는 테스트 케이스가 존재합니다.
  // 오름차순 정렬된 정수의 배열(arr)과 정수(target)를 입력받아 target의 인덱스를 리턴해야 합니다.
  // 이진탐색 알고리즘(O(logN))을 사용해야 합니다.
  // target이 없는 경우, -1을 리턴해야 합니다.
  // findIndex 를 사용한다.
  // let isTrue = (element) => element === target
  // arr를 절반으로 나눠서 진행을 한다
  // arr.length를 절반으로 나눈다 => 짝수의 경우에는 정확하게 반으로 나눠짐
  // arr.length / 2의 인덱스가 target보다 큰 지 여부를 확인한다.

첫 수도코드 => 시간복잡도가 O(logN)가 되기 위해서 제가 생각했던 방법은 배열을 중간 인덱스를 기준으로 나눠서 findindex를 진행했습니다. => 앞부분 배열의 경우에는 제대로된 인덱스를 리턴을 했는데, 배열 뒷부분에서 인덱스를 리턴할 경우 테스트가 통과되지 않았습니다. 그래서 앞배열의 길이를 더해주니 실행시간이 초과했습니다.

레프런스의 수도코드.

// 레프트는 0, 라이트는 배열의 마지막 인덱스를 할당한다.
// 배열의 중간번째 인덱스를 찾는다. 레프트랑 라이트를 더하고 2로 나누면 중간 인덱스가 나온다.
// 중간이 target과 동일하다면, 인덱스를 리턴한다.
// 만약에 타겟보다 중간의 값이 더 크다면, 
// 해당 인덱스보다 더 앞의 인덱스에서 찾으면 되기 때문에 레프트는 그대로두고, 라이트를 미들 - 1로 한다.
// 이를 통해서 미들 이전의 요소들에만 접근할 수 있다.
//미들의 같이 더 크다면,
// 라이트는 그대로두고, 레프트를 미들에서 1을 더해서 진행한다.
// 이를 통해서 미들 이후의 요소들에만 접근할 수 있다.

해당 문제에서, 이진탐색트리를 사용하라는 사항을 단순히 절반만 나눠서 진행을 하면 되지 않겠나라고 접근을 했었습니다. 근데 배열이 순서대로 정렬되어 있고, 중간값을 정해서 전반에는 작은 값, 후반에는 큰 값으로 이뤄져있기에 이진탐색트리로 적용이 된다는 걸 깨달을 수 있었습니다.

 

절반을 나눈다는 접근은 괜찮았다는 생각을 하지만, 이진탐색트리를 정확하게 이해하고  활용하지 않았다는 점에서 크게 아쉬움을 느낍니다. 

그래도 이를 통해서 이진탐색트리를 복습했다는 것에 위안삼고 다음에는 제대로 활용해봐야겠습니다 

 

=> 질의를 해놓은 상황이라 답변이 오면 추가적으로 작성하도록 하겠습니다.

이렇게 풀이를 진행할 경우, slice와 findIndex메소드가 시간복잡도를 올리는 요인이 됩니다.(배열의 앞쪽에 있다면 시간이 다소 줄어 들겠지만, 뒤에 있다면 처음부터 끝까지 findIndex 메소드를 사용하는 것과 크게 다르지 않을겁니다.)

코드스테이츠 측에서 온 답변입니다. 아직은 시간복잡도에 대해서 계산을 하는 게 어렵지만, 차차 연습해서 시간복잡도를 충족시키는 코드를 구현해야봐야겠습니다.


React 데이터 흐름

 

React에서의 데이터 흐름, 단방향 데이터 흐름을 이해할 수 있다.

컴포넌트는 부모 컴포넌트에서 props를 이용해 데이터를 마치 인자(arguments) 혹은 속성(attributes)처럼 전달받을 수 있습니다.(단, 컴포넌트는 props를 통해 전달받은 데이터가 어디서 왔는지 전혀 알지 못함)

 

=> 부모에서 자식으로 데이터가 흐르기 때문에 데이터 흐름은 하향식입니다. 이처럼 데이터는 한 방향(하향)으로 흐리기 때문에 단방향 데이터 흐름입니다.

 

React에서 어떤 데이터를 State로 관리해야하나?

State 조건!

- 부모로부터 props를 통해 전달됩니까? 그러면 확실히 state가 아닙니다.

- 시간이 지나도 변하지 않나요? 그러면 확실히 state가 아닙니다.

- 컴포넌트 안의 다른 state나 props를 가지고 계산 가능한가요? 그렇다면 state가 아닙니다.

 

어떤 컴포넌트에 state가 위치해야 하는지 알 수 있다.

상태가 특정 컴포넌트에서만 유의미하다면 => 특정 컴포넌트

하나의 state를 기반으로 두 컴포넌트가 영향을 받는다 => 두 컴포넌트를 소유하는 부모 컴포넌트

 

State 끌어올리기(Lifting state up)의 개념을 이해할 수 있다.

자식 컴포넌트에서 어떤 이벤트가 발생할 경우, 부모 컴포넌트의 state가 바뀌어야 한다면,

=> state를 변경할 수 있는 함수(Handler)를 props로 전달해줘서 이벤트를 통해서 해당 함수를 실행!

(컴포넌트는 자신의 state를 자식 컴포넌트에 props로 전달할 수 있습니다.)

 

출처

FormattedDate 컴포넌트는 date를 자신의 props로 받을 것이고 이것이 Clock의 state로부터 왔는지, Clock의 props에서 왔는지, 수동으로 입력한 것인지 알지 못합니다.

이를 “하향식(top-down)” 또는 “단방향식” 데이터 흐름이라고 합니다. 모든 state는 항상 특정한 컴포넌트가 소유하고 있으며 그 state로부터 파생된 UI 또는 데이터는 오직 트리구조에서 자신의 “아래”에 있는 컴포넌트에만 영향을 미칩니다.

트리구조가 props들의 폭포라고 상상하면 각 컴포넌트의 state는 임의의 점에서 만나지만 동시에 아래로 흐르는 부가적인 수원(water source)이라고 할 수 있습니다.

단방향 데이터 흐름이라는 원칙에 따라, 하위 컴포넌트는 상위 컴포넌트로부터 전달받은 데이터의 형태 혹은 타입이 무엇인지만 알 수 있습니다. 데이터가 state로부터 왔는지, 하드코딩으로 입력한 내용인지는 알지 못합니다.
그러므로 하위 컴포넌트에서의 어떤 이벤트로 인해 상위 컴포넌트의 상태가 바뀌는 것은 마치 "역방향 데이터 흐름"과 같이 조금 이상하게 들릴 수 있습니다. React가 제시하는 해결책은 다음과 같습니다.

상위 컴포넌트의 "상태를 변경하는 함수" 그 자체를 하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행한다

여전히 단방향 데이터 흐름에 원칙에 부합하는 해결방법입니다. 바로 이것이 "상태 끌어올리기" 입니다.


Effect Hook

 

Side effect가 어떤 의미인지 알 수 있다.

Side Effect (부수 효과)

함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우 해당 함수는 Side Effect가 있다고 이야기합니다.

 

- Side effect의 예 -
타이머 사용 (setTimeout)
데이터 가져오기 (fetch API, localStorage)

 

Pure Function (순수 함수)

순수 함수란, 오직 함수의 입력만이 함수의 결과에 영향을 주는 함수를 의미합니다. 함수의 입력이 아닌 다른 값이 함수의 결과에 영향을 미치는 경우, 순수 함수라고 부를 수 없습니다. 또한 순수 함수는, 입력으로 전달된 값을 수정하지 않습니다.

function upper(str) {
  return str.toUpperCase(); // toUpperCase 메소드는 원본을 수정하지 않습니다 (Immutable)
}

upper('hello') // 'HELLO'

순수 함수에는 네트워크 요청과 같은 Side Effect가 없습니다. 순수 함수의 특징 중 하나는, 어떠한 전달 인자가 주어질 경우, 항상 똑같은 값이 리턴됨을 보장합니다. 그래서 예측 가능한 함수이기도 합니다.

 

  • React 컴포넌트를 만들 때 side effect로부터 분리해서 생각할 수 있다. (비즈니스 로직과 표현 영역 구분)

useEffect

useEffect는 컴포넌트 내에서 Side effect를 실행할 수 있게 하는 Hook 입니다. 

출처

HOOK규칙
최상위(at the Top Level)에서만 Hook을 호출해야 합니다
반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출하지 마세요. 
대신 early return이 실행되기 전에 항상 React 함수의 최상위(at the top level)에서 Hook을 호출해야 합니다.
이 규칙을 따르면 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장됩니다.
이러한 점은 React가 useState 와 useEffect 가 여러 번 호출되는 중에도 Hook의 상태를 올바르게 유지할 수 있도록 해줍니다. 이 점에 대해서 궁금하다면 아래에서 자세히 설명해 드리겠습니다.

오직 React 함수 내에서 Hook을 호출해야 합니다
Hook을 일반적인 JavaScript 함수에서 호출하지 마세요. 대신 아래와 같이 호출할 수 있습니다.

✅ React 함수 컴포넌트에서 Hook을 호출하세요.✅ Custom Hook에서 Hook을 호출하세요. (다음 페이지에서 이 부분을 살펴볼 예정입니다)
이 규칙을 지키면 컴포넌트의 모든 상태 관련 로직을 소스코드에서 명확하게 보이도록 할 수 있습니다.
useEffect( () => {}, []) => 해당 컴포넌트가 실행될 때. => componentDidMount
useEffect( () => { return function() {~~~~}, []) => 해당 컴포넌트가 끝날 때 실행.
=> componentWillUnmount

컴포넌트 LifeCycle

class형 컴포넌트
componentDidMount(){
} => 처음 DOM에 렌더링 될 때 실행되는 함수.

componentWillUnmount() {
} => 해당 컴포넌트에 의해 생성된 DOM이 삭제될 때 실행되는 함수.

 

useEffect의 첫번째 인자는 함수입니다. 해당 함수 내에서 side effect를 실행하면 됩니다. 이 함수는 다음과 같은 조건에서 실행됩니다.

  • 컴포넌트 생성 후 처음 화면에 렌더링(표시)
  • 컴포넌트에 새로운 props가 전달되며 렌더링
  • 컴포넌트에 상태(state)가 바뀌며 렌더링

이와 같이 매 번 새롭게 컴포넌트가 렌더링될 때 Effect Hook이 실행됩니다.

 

useEffect두 번째 인자는 배열입니다. 이 배열은 조건을 담고 있습니다. 여기서 조건은 boolean 형태의 표현식이 아닌, 어떤 값의 변경이 일어날 때를 의미합니다. 따라서, 해당 배열엔 어떤 값의 목록이 들어갑니다. 이 배열을 특별히 종속성 배열이라고 부릅니다.

 

useEffect(함수, [종속성1, 종속성2, ...])

 

배열 내의 종속성1, 또는 종속성2의 값이 변할 때, 첫 번째 인자의 함수가 실행됩니다.

배열 내의 어떤 값이 변할 때에만, (effect가 발생하는) 함수가 실행됩니다.

 

단 한번만 실행되는 Effect 함수

만일 종속성 목록에 아무런 종속성도 없다면 어떤 일이 발생할까요? 달리 말해, 두 번째 배열을 빈 배열[]로 둘 경우에는 무슨일이 발생할까요? 두 번째 인자를 아예 안넘기는 것과 어떻게 다를까요?

 

1. 빈 배열 넣기 => 컴포넌트가 처음 생성될 때만 함수가 실행

useEffect(함수, [])

2. 아무것도 넣지 않기 (기본 형태) => 컴포넌트 생성, props, state 업데이트 될 때 함수가 실행

useEffect(함수)

3. 종속성 값을 배열에 넣기 => 배열 내의 어떤 값이 변할 때만 함수가 실행

useEffect(함수, [종속성])

 

 

(2번) 기본 형태의 useEffect는 컴포넌트가 처음 생성되거나, props가 업데이트되거나, 상태(state)가 업데이트될 때 effect 함수가 실행됨을 앞서 배웠습니다.

 

반면에 (1번) 빈 배열을 useEffect의 두 번째 인자로 사용하면, 이 때에는 컴포넌트가 처음 생성될때만 effect 함수가 실행됩니다. 이것이 언제 필요할까요? 대표적으로 처음 단 한 번, 외부 API를 통해 리소스를 받아오고 더이상 API 호출이 필요하지 않을 때에 사용할 수 있습니다.

 

컴포넌트 내에서 AJAX 요청

목록 내 필터링을 구현하기 위해서는 다음과 같은 두가지 접근이 있을 수 있습니다.

  1. 컴포넌트 내에서 필터링: 전체 목록 데이터를 불러오고, 목록을 검색어로 filter 하는 방법 => 처음 단 한번, 외부 API로부터 명언 목록을 받아오고, filter 함수를 이용합니다.
  2. 컴포넌트 외부에서 필터링: 컴포넌트 외부로 API 요청을 할 때, 필터링한 결과를 받아오는 방법 (보통, 서버에 매번 검색어와 함께 요청하는 경우가 이에 해당합니다) => 검색어가 바뀔 때마다, 외부 API를 호출합니다.

두 방식의 차이점

장점단점

컴포넌트 내부에서 처리 HTTP 요청의 빈도를 줄일 수 있다 브라우저(클라이언트)의 메모리 상에 많은 데이터를 갖게 되므로, 클라이언트의 부담이 늘어난다
컴포넌트 외부에서 처리 클라이언트가 필터링 구현을 생각하지 않아도 된다 빈번한 HTTP 요청이 일어나게 되며, 서버가 필터링을 처리하므로 서버가 부담을 가져간다

 

AJAX 요청이 매우 느릴경우?

모든 네트워크 요청이 항상 즉각적인 응답을 가져다주는 것은 아닙니다. 외부 API 접속이 느릴 경우를 고려하여, 로딩 화면(loading indicator)의 구현은 필수적입니다.

이처럼 State를 만들어서 상태를 관리할 수 있습니다.

const [isLoading, setIsLoading] = useState(true);

// 생략, LoadingIndicator 컴포넌트는 별도로 구현했음을 가정합니다
return {isLoading ? <LoadingIndicator /> : <div>로딩 완료 화면</div>}

fetch 요청의 전후로 setIsLoading을 설정해주어 보다 나은 UX를 구현할 수 있습니다.

useEffect(() => {
  setIsLoading(true);
  fetch(`http://서버주소/proverbs?q=${filter}`)
    .then(resp => resp.json())
    .then(result => {
      setProverbs(result);
      setIsLoading(false);
    });
}, [filter]);
Comments