20) 검색할때 바로바로 안걸러주면 한국인은 답답해서 못살아 🤯 검색 프로세스 이해, Tokenizing, inverted index.. Debouncing / Throttling 원두멘토님의 열정을 이어받아 다 이해한날 !!!!!!

김아름·2022년 4월 8일
0

코드캠프6기

목록 보기
20/36
post-thumbnail

썸넬은 요즘 프론트 6기의 필수품 졸음깨는껌 ! 돌아다니면서 조는사람 나눠주는 따듯한 코캠러~
근데 다들 하나씩 가지고 있는게 웃겨 증말 !

오늘은 먼가 지치는 금요일 아침 ..
알고리즘 한문제도.. 안풀리는 아침..금요병인가?
멘토님들은 항상 쌩쌩하신것 같다..할일도 많으신데..대단하다...

  • 오늘의 요약 (오늘이 지나면 다 이해가능한 것들!)
    오늘은 검색 프로세스에 대해 배웠습니다!
    먼저, 백엔드의 검색 시스템 구조에는 가장 기본적으로 테이블을 풀 스캔하는 방식(full table scan)으로 전체 테이블 로우를 조회하는 방법이 있었습니다!
    초기에 빠르게 만들기 위한 방법으로 사용되고, 서비스 규모가 커짐에 따라 이러한 방식은 잘 사용되지 않습니다!
    이 방법을 개선하기 위해 데이터베이스에 저장할 때, 문장을 키워드 단위로 토크나이징(컴퓨터에게 이해 시키기 위해 우리의 언어를 의미가 있는 가장 작은 단어로 나누는 것)하고, 역인덱스(inverted index)를 만들어서 저장했습니다.
    이를 쉽게 해주는 데이터베이스 프로그램이 엘라스틱서치(ES)였죠!
    마지막으로, 서비스가 더 커지게 되면, 수많은 사람들이 검색하는 데이터는 어느정도의 틀에서 크게 벗어나지 않는다 했습니다!
    따라서, 검색어와 매칭되는 검색결과를 메모리에 저장(검색로그 캐싱) 후, 빠르게 찾아쓸 수 있도록 도와주는 데이터베이스 프로그램이 존재하는데 이것이 레디스(redis)입니다!
    위 두 데이터베이스의 가장 큰 차이는 저장 방식이였습니다!
    redis는 휘발성 데이터 저장방식 memory기반, 엘라스틱서치는 하드에 저장시켜 데이터가 보존되는 disk기반이였죠!?
    두 방식 모두 장단점이 존재했습니다! 상황에 따른 활용으로 성능을 더욱 효율적으로 끌어올릴 수 있다는걸 기억하셔야 합니다!
    프론트엔드에서 검색 기능을 수행하기 위해 api 요청 시 검색 키워드를 함께 넘겨주었죠!? 여기서 핵심은 검색 결과와 페이지네이션과의 관계였습니다!
    검색어를 기준으로 페이지네이션이 새롭게 만들어져야했죠? 따라서 우리는 검색어를 기준으로 refetch를 실행해주었습니다!
    여기서 문제가 있었죠!? 검색어를 변경하고 검색을 누르지 않았는데 페이지를 이동했을때, 검색어가 변경된다는 점 이였습니다! 이를 해결하기 위해 state를 search와 keyword로 나누어 주었습니다!!
    마지막으로 검색 버튼 없이 검색하는 방법에 대해서 배웠는데 여기서도 문제가 하나 있었습니다! 검색어를 하나 입력할 때 마다 gql요청이 연속적으로 보내졌죠? 이렇게 되면 백엔드 컴퓨터의 메모리,cpu 낭비가 심해져 효율이 떨어진다 했습니다!
    이 때, 연속적으로 요청되는 쿼리를 막기 위해서 디바운싱 이라는 기술을 활용했습니다! 디바운싱은 반복적인 요청을 일정한 텀을 가지고 한번에 묶어서 요청해주는 것! 기억나시죠!?
    반대로 쓰로틀링도 있었습니다! 쓰로틀링이란 마우스 이동, 스크롤 이동을 감지하면 특정 이벤트를 실행시키고, 이동이 멈출때까지, 이벤트가 재실행 되지 않도록 막기 위해 사용했습니다.
    이러한 기술을 쉽게 사용 가능하도록 자주 사용되는 라이브러리가 바로 lodash 였습니다!
    마지막으로 검색어에 해당하는 단어의 색을 변경했습니다!
    replaceAll을 통해 검색어 키워드 앞,뒤에 시크릿 코드를 붙여 split을 사용해 잘라낸 후 map으로 화면에 출력해주었죠!?
    replaceAll -> split -> map 흐름을 기억하시길 바랍니다!

- 포트폴리오 리뷰

  • 게시글 작성에서 이미지 업로드해서 주소값까지 보내는 과정 !
    • 게시글 등록에서 사진 첨부하는 부분
      div 하나를 map으로 돌리는걸 볼수 있다
      현재는 아무것도 없는 빈 문자열 3개가 있는 배열 안에 넣어줄예정이다

      키를 넣어줄때 uuid로 키를 잡아놨다
      fileurl 배열안에 들어있는 각각이 el이 되는거고, 그 각각에 파일url을 넣어줄거다
      이해가 잘가도록 좀더 자세히!

      실제 파일 첨부하는 애는 숨겨져 있고, 버튼을 보여줘서 밑에 첨부하는 애가 눌리도록 설정 !

      사진을 첨부하면 되어야 할것 - checkvalidation, 등록페이지 상자가 이미지로 바뀌는것
      파일이 바뀌면 실행되는 함수 -검증을하고, 파일을 result에 담고
      강아지 이미지 주소, 인덱스를 온체인지 파일유알엘에 넘겨줘따 , 인덱스는 차례로 0,1,2가 되는거고 !
      하나의 이미지가 contatiner와 prsenterrk 각각 있다
      boardwrite에 onchangefileurl함수는 (그 인덱스에 파일을 넣으려고 하는작업 )

      fileurl이 원래 ["","",""] 이거였는데 첫번째가 강아지.png로 바뀜
      업로드 컨테이너가 props로 도그를 받고, 걔를 이미지 업로드의 부분으로 보내주고,
      프리젠터에서
      fileurl이 있으면 그 주소 props.fileurl보여주고,
      fileurl이 없으면 그냥 빈 div보여줘라고 바꾸면 된다
      얘 클릭해서 바꾸고 싶을때 그냥 보여주는 함수 넣어주면 된다. 어차피 hidden버튼이 눌리게 될꺼고 이미지는 보여지는 기능만 가지고 있다.
      fileurls[index] = fileurl 이거 이해하자 !
      얕은복사의 개념과, newfileUrls=[...fileUrls] 이부분 제대로 이해하자

      - 상세보기에서 이미지 보여주는 부분
      boarddetail부분에서 주소값만 받아서 띄워주면 된다 !
      container에서 fetchboard 했을때 이미지를 추가로 받아와야 하고,
      presenter에서 props로 보여주면 되는데...!
      filter을 해서 빈 문자열이 아닌애들만 필터해줘! 라고 한 것
      그래서 이를 바탕으로 상세보기에서 데이터 있는애만 보여지게 된것.!


      - 수정하기에서 업로드된 이미지로 바꾸어주는 부분
      default벨류로 원래 사진이 들어가게 된다. default value를 어떻게 보여줬을까?
      State안에 값응ㄹ 넣어줘버리면 처음엔 props.data가 없기 때문에 안그려진다
      처음엔 이렇게 빈문자열로 줄수밖에 없음!

      그래서 usEffect를 써주고 , 데이터가 변경이 되면, 패치보드가 추가가되고, 거기에 length가 있으면

      그 이미지를 setfileurls에다가 넣어줘! 라는 뜻이다
      수정하기를 하면 안에있는 fileurl만 바꿔주는 함수당,
      처음에 해당페이지에서 fetchboard해서 이미지 불러오기,
      그 이미지를 defaultvalue로 주기위해서 useEffect를 사용하기,
      화면에 사진이 나오기,
      변경하는것은 함수를 넣어주고 등록하기 화면이랑 동일하게 진행 !


      - 수정하기 버튼 눌렀을때 이미지가 변경이 되었는지, 안변경되었는지 체크하는 방법

      위에는 title contents...변경된게 있으면 추가해주는 형식이었다.
      이미지도 변경된게 있으면 추가해주라고 쓰면 된다
      변경이 된건지 안된건지를 알기위해 isChangedFiles를 만들어서

      조금 생소한 방식으로 비교했는데, 각각의 상태를 변수에 지정해준것일뿐
      문자열로 바꾼다음에 비교를 한것이다
      지금 현재 fileurl과 fetboard에서 받은 image(디폴트) 그게 같으면 현재파일,,아니면 디폴트 줘라

      영상을 다시 보면서 이해해야 할듯하다 흐름이 한번에 들어오진 않지만 그래도 대충 어떤 방식인지 이해는 가니까

난 분명 다 이해할수 있어 !

- 검색 프로세스 이해

  • 검색을 이해하려면 다양한 DB를 알아야돼 !
    프론트에선 search만 붙여서 보내주면 되지만, 백엔드에서 무슨일이 일어나는지 이해하자 !

    search라고 단어가 들어오면 Board.find()라고 찾으면 되지 !
    (우리 api만들때 board.insert board.update했던것처럼 !)찾아서 목록을 프론트에 보내주면 되는거 아닌가?

    근데 이렇게 하면 67만2번째에 있으면 어쩔꺼야?!
    엄청 오래걸리고 비효율적이겠지?! 다 훑어야 하니까 !!
    그런 방식을 table fullscan! 이라고 한다 위에서부터 하나씩 다 찾아 내려가는것 ...!

    그래서 구글 검색엔진의 시작과도 같은 방법, 똑똑한 방법이 있는데,
    원래 페이지말고 검색용 페이지를 하나 더 만든거야 ! 검색용 페이지에다가는 키워드만 쏙쏙 단어만 넣은거지 !

    단어들을 쪼개서 테이블을 저장한 방법 ! 그 자른 단어들을 토큰이라고 부르거든?

문장을 토큰으로 자르는 과정 ! -> 토크나이징 Tokenizing

그 자른 토큰들로 만든 검색 테이블 -> 역색인(역인덱스) inverted index

역색인 구성 테이블의 구조예시!

애초에 이러한 목적으로 만들어진 테이블을 ElasticSearch


잠시 개념 알고가기 검색 백엔드에서 우찌 이루어지는지는 알아야 검색기능 같이 협업하지 !

1. Elasticsearch란?
Elasticsearch는 Apache Lucene( 아파치 루씬 ) 기반의 Java 오픈소스 분산 검색 엔진입니다.
Elasticsearch를 통해 루씬 라이브러리를 단독으로 사용할 수 있게 되었으며, 방대한 양의 데이터를 신속하게, 거의 실시간( NRT, Near Real Time )으로 저장, 검색, 분석할 수 있습니다.

2. 디스크기반 데이터 베이스
c드라이브같ㅇㄴ데다가 저장해놓고, (영구적으로 보관) 저장하는데 시간 오래걸리고, 용량도 크고 ㅎ기 때문에 좀 비효율적이다 그래서 비효율적이다

3. 메모리기반 데이터 베이스
정보들을 Ram(메모리, 즉 변수)에다가 저장하는 방식이다
Redis Memcached 개념 알아두기
4. 자주 검색되는 검색로그들은 메모리에 따로 저장(자주 검색되니까 굳이 메모리에 접근해서 가져올필요없음)
사용자들이 검색을 많이 하다보면 어느정도 통계가 나오기때문에
검색로그캐싱(임시저장정도로 생각)을 해놓는다

- 검색 결과를 페이지네이션과 연결해야해 !

Search / pagination

- 검색 기능 구현

검색한다는건 검색어를 포함해서 다시 검색한다고 생각하면 된다, 그래서 리패치를 하면돼 ! 같은거야 !
api가면 search부분 있으니까 gql에 넣어준다
usequery로 받은 data도 state라서 값이 바뀌면 return 부분이 다시 그려진다 !
input 태그에서 받은값을 저장해놓을 함수도 만들어준다 (끄 변경 이벤트 값으ㄹ가지고 검색해야하니까 ! )
그래서 refetch까지 다 해줬는데, 검색한 페이지는 어떻게 보여주까?

- 검색하기 버튼 없이 검색을 한다고?

-> Debouncing / Throttling

  • 버튼을 그냥 숨기고 onchange일어날때마다 refetch하면, 바로 계속 리패치는 되고 가장 쉬운 방법이긴 하지만, 계속 graphql요청이 들어가기때문에 좋은 방법이 아니다.

- 디바운싱 (Debouncing)

특정작업의 특정시간내에서 그 작업이 반복되지 않으면 마지막만 데이터를 전송하는것
-> 쉽게 생각하면, 검색기능에서 검색어 '메'까지 입력하고 2초설정해놓은 동안 아무것도 안일어나면 '메'로 검색 refetch 1번 해주는것

- 쓰로틀링 (Throttling)

먼저 한 번 실행 후 , 특정시간동안 무시
->쉽게 생각하면, 무한스크롤에서 처음에 시행해서 패치하고 그 시간이 끝나면 패치. 스크롤이 조금 내려가면! 이라는 함수가 있는데, 계속 내려도 좀 무시

- lodash

  • 이제 로데쉬 라이브러리는 안쓰는 회사는 거의 없엉..4천만다운이 되었는데?!
    그러니까 독스를 보고 익히는것이 좋다! 사라질일이 없으니까 ?1
    안보면 계속 안보게 된다.. 원두멘토님이 강조하시는 이유가 있어! 그니까 독스랑 친하게 지내는 습관을 들이자고!?
    우리는 지금 lodash안에있는 기능을 사용하려고 한다 !
    debounce 기능을 사용하는건데 !
  1. 검색한 값을 debounce에 담아서 refetch로 보내주면 저기 0.2초 동안은 refetch를 무시하는 기능이 있는거다 .
  2. 그러니까 값을 저장해놓는 state들도 필요가 없어지는셈 ! const [search, setSearch] = useState("")

- 검색어(keyword) 색깔만 다르게 바꿔주는 기능

title은 한문장인데 그 중에 하나만 뽑아서 바꾼다 ?
-> title을 문자열 단위로 다쪼개고,, 각각을 태그로 만들어준후 해당 태그에 이모션을 달아준다


예) 점심이 키워드라면 걔를 구분자로 정해주고 빨간색으로 바꿔서 사이에 토큰으로 다시 넣어줄꺼야!

다른 훨씬 더 깔끔하고 예쁜예) 구분자에 우리만 알수있는걸(시크릿코드)를 넣어주고 걔만 뺀다
그러면 그 토큰만을 제외하고 똑 나누어지겠지 !
1. 점심 양 옆에 시크릿코드를 붙여줄거야
2. 오늘 철수와 점심 먹었습니다.replaceAll("점심","#%점심#%")
3. 원본의 점심이라는 단어만 바꿔줬으니까 그걸 기준으로 split
4. 그 점심을 state에 저장한다 (keyword)

  • keyword state를 만든다
  • replaceAll 과 split을 사용
  • 검색기능 키워드 글자색 변경까지 완료한 코드

    import {useQuery,gql} from '@apollo/client' 
    import styled from '@emotion/styled'
    import { ChangeEvent, useState} from 'react'
    import { IQuery, IQueryFetchBoardsArgs } from '../../src/commons/types/generated/types'
    import _ from "lodash";
    import {v4 as uuidv4} from "uuid";
    const FETCH_BOARDS = gql`
    
      query fetchBoards($search: String, $page :Int){
              fetchBoards(search : $search, page : $page ){
                  _id
                  writer
                  title
                  contents
    
              }
      }
    `
    const MyRow = styled.div`
    display: flex;
      flex-direction: row;
    `
    const MyColumn = styled.div`
      width: 25%;
    `
    interface Iprops{
    
      isMatched:boolean;
    }
    const Word = styled.span`
      color : ${(props : Iprops)=> (props.isMatched ? "blue" : "black")};
    `;
    export default function SearchPage(){
          const [keyword, setKeyword] = useState("")
    
      
          const { data, refetch } = useQuery<Pick<IQuery,"fetchBoards">,IQueryFetchBoardsArgs>(FETCH_BOARDS)
    
          const getDebounce = _.debounce((data)=>{
              refetch({ search : data, page : 1})
              setKeyword(data);
          },200)
    
          const onChangeSearch = (event:ChangeEvent<HTMLInputElement>)=>{
              getDebounce(event.target.value)
    
          }
          // const onClickSearch = ()=>{
          //     refetch({ search : search , page : 1})
          //     // 숏핸드프로퍼티로 인해 한줄만 써도 됩니당 근데 왜 그렇게 쓰는지는 알아야쥐
          // }
    
          const onClickPage = (event : any) =>{
    
              refetch({page:Number(event.target.id)}) 
              
    }
return(
<>  
    검색어 입력 : <input type="text" onChange={onChangeSearch}/>
    {/* <button onClick={onClickSearch}>검색하기</button> */}
    {data?.fetchBoards.map((el : any)=>(
        <MyRow key={el._id}>
            <MyColumn>{el.writer}</MyColumn>
            <MyColumn>
                {el.title
            .replaceAll(keyword,`#$%${keyword}#$%`)
            .split("#$%")
            .map((el)=>(
            <Word key ={uuidv4()} isMatched={keyword === el}>
                {el}
                </Word>
                ))} 
                </MyColumn>
        </MyRow>
    ))}
    {new Array(10).fill(1).map((el,index)=> (
        <span key={index + 1} id = {String(index + 1)} onClick={onClickPage}>{index + 1}</span> 
        ))
    }
</>
)


}```
  • 알고리즘 수업
    오늘 못푼문제 ㅠㅠ 설명 들으니까 충분히 이해가간다..!
    생각하는 능력 1 1 ! !

    유클리드 호제법 풀이

    베이직한 방법 풀이

profile
SUNNY SUMMER ! 같이 일하고 싶은 개발자 여름이의 초심을 잃지 않기 위한 주절주절 부트캠프 시절 블로그.

0개의 댓글