합격 발표가 난 지 한 달이 넘었지만, 그동안 다른 일 한다고 미루다 이제야 써봅니다.

 

  저는 학원 1차 스터디에서 ADsP 스터디를 했었고, 1주일에 한 3~4시간씩, 약 두 달 정도 시험을 준비했습니다. 시험일이 1차 프로젝트 기간이랑 겹쳐서 시험 직전에 공부를 많이 하지는 못했지만, 그래도 프로젝트 전에 책을 1회독 정도 해놔서 시험 전날 밤을 새워서 벼락치기를 해서 시험장에 들어갔습니다. 운이 좋게 합격을 하긴 했지만, 다른 분들 보다 시간을 너무 많이 쓰고 합격한 것 같아서 어떻게 하면 시간을 효율적으로 사용해서 단기간에 합격할 수 있을까 나름대로 고민을 많이 했습니다.

 

  제 점수입니다. 엄청 고득점은 아니지만 시험 커트라인이 60점이니 70점대로 합격하는 게 가장 이상적인 상황이 아닐까 싶습니다. 일단 제가 추천드리는 책은 이지패스에서 나온 미어캣이 그려져 있는 교재입니다.

 

  굳이 종이책으로 살 필요는 없고, ebook으로 구입하시면 저렴하게 구입하실 수 있습니다. 그리고 알라딘 앱으로 접속하셔서 각종 적립금을 받고 구입하신다면 몇 천 원 정도 더 할인된 가격으로 구입하실 수 있습니다.

  아니면 밀리의 서재 같은 ebook 플랫폼에서 ADsP로 검색을 하면 교재가 한두 권 정도 나옵니다. 물론 이런 곳에 올라온 교재들은 최신 교재는 아니지만, 시험 범위가 크게 바뀌는 것이 아니라면 작년이나 재작년 교재 정도는 봐도 괜찮을 것 같습니다.

  제가 미어캣 교재를 추천하는 이유는 어플로 문제를 많이 제공해 주기 때문인데요. 책은 20일 분량으로 되어있지만 진짜 20일 안에 다 보기에는 너무 분량이 많습니다. 그리고 어떤 부분을 집중적으로 공부해야 하는지도 막막할 것입니다. 그래서 제가 추천드리는 방법은 다음과 같습니다.

 

1. 데이터 분석 부분을 빠르게 훑어본다. (어떤 내용이 있는지 정도만)

2. 교재의 문제와 어플의 문제를 풀어보면서 기출 된 문제와 문항을 교재에 표시한다.

3. 데이터 분석 기획 / 데이터 분석 부분도 같은 방식으로 표시한다.

4. 시험 전에 표시한 부분만 반복해서 본다.

 

  예를 들면 이런 식인데요. 문제를 반복해서 풀다 보면 자주 출제되는 부분이 어디인지, 자주 바꿔서 내는 문항이 어떤 것인지 금방 파악할 수 있습니다. 물론, 가장 어려운 데이터 분석 파트는 범위가 많기 때문에 시간이 오래 걸릴 수도 있는데요. 1과목과 2과목을 이런 방식으로 빠르게 공부하시고, 3과목은 시간을 더 투자하셔서 꼼꼼하게 보셔도 좋을 것 같습니다. 사실 3과목 내용이 어려운 편이라 공부를 열심히 하고 들어가도 막상 시험문제를 보면 이게 공부한 내용인지 애매한 경우가 많습니다. 그래서 1과목과 2과목에서 최대한 많이 맞는 것을 목표로 하고, 3과목은 사실상 운에 맡기는 것을 추천드립니다. 그렇다고 3과목 공부를 게을리하라는 얘기는 아닙니다. 3과목 공부를 열심히 해야 운도 따라줍니다.

 

  그리고 유튜브를 보면 4~5시간 분량으로 ADsP 요약을 해주는 강의들이 많습니다. 저도 시험 전에 그 영상들을 봤는데요. 아예 베이스가 없는 상태에서 강의만 들으면 솔직히 합격에 도움이 될까 싶습니다. 강의들 대부분이 내용을 자세히 설명 안 해주고 이 파트에서는 이 부분이 중요하다는 정도만 짚어주는 강의도 있고, 중요해 보이는 부분도 자세한 설명 없이 그냥 넘어가서 당황스러웠던 기억이 있습니다. 따라서, 유튜브 강의는 공부를 다 마치고 마무리용이나 참고용으로만 보시는 편이 좋을 것 같습니다. 아니면 유튜브 강의를 듣고 문제만 풀어도 될 것 같기도 합니다. 다만 한국데이터산업진흥원에서 주관하는 시험들이 문제를 공개하지 않기 때문에 기출문제를 구하기가 어려워서, 기출문제들을 쉽게 구할 수 있다면 굳이 교재를 사지 않고 유튜브를 보고 문제 풀어보는 식으로 공부해도 충분할 것 같습니다.

 

  제가 했던 공부방법이 정답이 아닐 수도 있고, 제가 추천드린 방법보다 더 좋고 효율적인 방법이 많을 것입니다. 각자 맞는 공부 방법이 다르기 때문에 참고용으로만 봐주시길 바랍니다. 8월 초에 제42회 ADsP 시험이 있는 걸로 알고 있는데, 열심히 공부하셔서 원하시는 결과 얻으시길 바랍니다. 

 

요약

1. 교재를 이용해서 공부한다면 이지패스의 미어캣 교재를 추천 (어플로 제공되는 문제 수가 많아서 좋음)

2. 굳이 교재를 안 사고 밀리의 서재 같은 곳에서 ADsP 교재를 찾아서 공부해도 됨 (옛날 책인 경우가 많아서 유튜브로 추가 학습 추천)

3. 교재로 공부한다면 교재를 빠르게 훑어보고 문제를 풀어본 후에, 문제가 출제된 부분을 교재에 표시함

4. 유튜브 강의는 책 한권을 다 보고 나서 시험 마무리용이나 참고용으로 추천

  2021년에 구입한 로지텍 G304마우스를 그동안 잘 쓰고 있었는데, 1년 정도 지나자 로지텍 마우스의 고질병인 더블 클릭 문제가 발생했고, 숙대입구역에 있는 A/S센터에 가서 새 제품으로 교환받았습니다. 그 후 2년 정도가 지나서 다시 왼쪽 버튼이 더블클릭되는 문제가 발생했습니다. 보증기간인 2년이 지나서 직접 스위치를 교체하는 방법을 찾아봤는데, 스위치를 구매한 후에 납땜을 해야 한다고 해서 포기하고 있다가 Aliexpress에서 G304용 납땜이 다 되어있는 스위치를 팔고 있어서 구매를 했습니다.

 

  코인샵에서 구매하면 약 3달러 정도에 구매할 수 있고, 저는 빨리 받기 위해서 배송비를 더 주고 빠른 배송방법을 선택했습니다. 그리고 다른 판매자들 보면 스위치 사진만 올려놓고, 스위치 이름을 A1, A2, A3,... 이런 식으로 표현해서 어떤 스위치인지 설명을 안 해주는 불친절한 판매자들이 많습니다. 저는 조금 더 비싸더라도 스위치 이름을 제대로 써준 판매자에게 구매했습니다. 6월 26일에 주문하고 약 10일 정도 후인 7월 5일에 배송받았는데, 알리 여름 세일 기간이라 통관 물량이 많았던 것을 고려하면 생각보다 빨리 도착했습니다.

 

  스위치를 구매하면, 이렇게 다 납땜이 된 상태로 도착합니다. 기존 스위치를 제거하고, 이 스위치를 그대로 조립하면 됩니다. 


조립 순서

  조립 방법은 다른 분들이 블로그에 친절하게 설명해 주셨기 때문에, 저는 간략하게 제가 헤맸던 부분만 올려보겠습니다.

 

  먼저 위, 아래 양쪽 고무들을 제거합니다. 고무를 제거할 때, 겉에 부분만 살짝 제거하면 고무가 두 개로 분리될 수 있기 때문에, 고무 끝에 손톱을 끝까지 넣어 한 번에 제거해야 합니다. 고무 부분은 한 번 제거하면 나중에 다시 붙여도 약간 우글우글해지기 때문에, 거슬리시는 분들은 고무만 따로 구입하거나 하셔도 좋을 것 같습니다.

  

  위에서 나사를 다 제거한 후에 마우스 버튼 부분부터 위로 들어 올려서 열어주고, 아래 부분은 수직으로 들어 올린다는 느낌으로 조심해서 열어줘야 합니다. 뒤로 젖히면 부러질 수 있기 때문에 이 부분은 조심해서 열어줘야 합니다. 분리됐으면, 상판과 하판이 연결되어 있는 핀 두 개를 제거해 줍니다.

 

  상판을 분리해 줬으면, 스위치 버튼과 연결되어 있는 붉은색 동그라미가 쳐져있는 나사 네 개를 제거합니다. 다른 분들의 글을 보면 상판의 가운데 부분에 있는 나사 세 개를 제거하라고 되어있는데, 저는 제거 안 하고 해도 잘 교체가 되었습니다. 특히 가운데 깊숙한 쪽의 나사는 한 번 분해하면 다시 조립하기도 힘들어서, 일단 나사 네 개만 제거하고 안되면 그때 제거해 보시기 바랍니다.

  나사 제거가 끝나면 나사가 있었던 판을 일자 드라이버나 납작한 물건으로 들어 올려줍니다. 그러면 마우스 버튼이 위로 들어 올려지는데, 마우스 버튼을 당겨서 천천히 뽑는다는 느낌으로 뽑아주세요.

 

  버튼을 뽑았으면 위 사진에서 보이는 나사 두 개를 제거해 주고, 스위치를 감싸고 있는 나사 네 개를 제거해 주세요.

 

  나사를 다 제거한 후에 기존의 스위치를 제거하고, 구입한 스위치를 설치해 줍니다. 가장 끝 쪽에 있는 구멍에 상판의 튀어나온 부분을 넣고, 나사를 조여주면 됩니다. 그리고 분해의 역순으로 다시 조립해 줍니다.

 

  전 조립을 마치니까 왼쪽 버튼이 살짝 더 올라와있네요. 사용에 지장은 없지만, 왼쪽 키감이 이전보다 안 좋아진 것 같은 느낌이 듭니다.


수리 후기

  카일 저소음 스위치가 좋다고 해서 사봤는데, 전에 사무실에서 사용하던 로지텍 M590과 느낌이 비슷한 것 같습니다. 소리가 거의 안 나서 조용하지만, 누르는 느낌이 거의 없어서 약간 심심하기도 합니다. 그리고, 휠 스위치는 교체하지 않아서 휠에서는 여전히 딸깍 소리가 납니다. 저는 G304는 주로 집에서 사용해서 굳이 저소음까지는 필요가 없었는데, 막상 교체하고 나니까 심심한 느낌이 강하네요. 다음에는 클릭음이 있는 스위치로 교체하거나, 요즘은 키보드만큼 마우스도 대체제가 많기 때문에 다른 마우스를 구입할까 고민 중입니다. 일단은 더블클릭 문제가 사라져서 만족스럽고, 다음에 또 스위치를 교체하게 된다면, 고무 부분도 따로 구입해서 붙여야겠습니다. 

  2차 프로젝트를 진행하면서 DB에 있는 전체 데이터를 보여주는 Table을 만들었습니다. MUI Data Grid와 같은 좋은 라이브러리들이 있지만, 아직 TypeScript를 공부하지 않아서 사용을 하지 못했습니다. 그래서 임시방편으로 table 태그를 이용해 직접 표를 만들었습니다. 코드가 지저분해지고 많은 기능을 넣을 수는 없어서 아쉬웠지만, 직접 table을 만들어 보는 것도 좋은 경험이었습니다.

  표를 다 만든 후에, th 항목에 마우스를 올리면 설명이 표시되게 하고 싶었는데, 마침 Radix Tooltip이라는 좋은 라이브러리를 찾아서 아래와 같이 구현할 수 있었습니다. 사용법도 간단합니다.


1. 설치

// install
npm install @radix-ui/react-tooltip

// import
import * as Tooltip from '@radix-ui/react-tooltip';

 

  터미널에 위와 같이 입력해서 설치한 후, Radix Tooltip을 사용할 페이지에서 import 해주세요.


2. 구조

import React from 'react';
import * as Tooltip from '@radix-ui/react-tooltip';
import { PlusIcon } from '@radix-ui/react-icons';
import './styles.css';

const TooltipDemo = () => {
  return (
    <Tooltip.Provider>
      <Tooltip.Root>
        <Tooltip.Trigger asChild>
          <button className="IconButton">
            <PlusIcon />
          </button>
        </Tooltip.Trigger>
        <Tooltip.Portal>
          <Tooltip.Content className="TooltipContent" sideOffset={5}>
            Add to library
            <Tooltip.Arrow className="TooltipArrow" />
          </Tooltip.Content>
        </Tooltip.Portal>
      </Tooltip.Root>
    </Tooltip.Provider>
  );
};

export default TooltipDemo;

 

  Radix Tooltip 페이지에서 제공하는 Demo Code입니다. Tooltip.Provier 태그가 전체를 감싸고 있고, 그 아래를 Tooltip.Root 태그가 감싸고 있습니다. Tooltip.Root 태그는 Tooltip.Trigger, Tooltip.Portal 태그를 감싸고 있는 구조입니다. 정리하면 다음과 같습니다.

 

  • Tooltip.Provider : 전체 Tooltip을 감싸는 태그
  • Tooltip.Root : Tooltip이 표시될 th 하나를 감싸는 태그
    • Tooltip.Trigger : Tooltip이 작동될 th 요소가 있는 태그
    • Tooltip.Portal : Tooltip event가 작동되면 표시될 메시지가 있는 Tooltip.Content 태그를 감싸는 태그

  전체적인 구조는 위와 같고, 사용하실 때는 Tooltip.Provider로 Tooltip이 들어갈 태그 전체를 감싸신 후에, th 하나마다 Tooltip.Root로 감싸주고, 그 안에 Tooltip.Trigger와 Tooltip.Portal로 나눠주시면 됩니다.


3. 사용 예시

<Tooltip.Provider delayDuration={0}>                              // Provider
  <Tooltip.Root>                                                    // Root 1
    <Tooltip.Trigger asChild>                                         // Trigger
      <th
        className='academyCount'
        onClick={(e) => handleThClick(e.target.className)}
      >
        *학원 수{getSortIcon('academyCount', sortColumn, sort)}
      </th>
    </Tooltip.Trigger>
    <Tooltip.Portal>                                                   // Portal
      <Tooltip.Content className='TooltipContent' sideOffset={10}>
        학원 수 (평생직업 교육학원), 자치구 기준
        <Tooltip.Arrow className='TooltipArrow' />
      </Tooltip.Content>
    </Tooltip.Portal>
  </Tooltip.Root>
  <Tooltip.Root>                                                    // Root 2
    <Tooltip.Trigger asChild>                                          // Trigger
      <th
        className='libraryCount'
        onClick={(e) => handleThClick(e.target.className)}
      >
        *도서관 수{getSortIcon('libraryCount', sortColumn, sort)}
      </th>
    </Tooltip.Trigger>                                                  // Portal
    <Tooltip.Portal>
      <Tooltip.Content className='TooltipContent' sideOffset={10}>
        도서관 (공공도서관, 작은도서관, 장애인도서관)
        <Tooltip.Arrow className='TooltipArrow' />
      </Tooltip.Content>
    </Tooltip.Portal>
  </Tooltip.Root>
</Tooltip.Provider>;

 

  예시 코드를 보시면, 전체 Tooltip 태그를 Tooltip.Provider가 감싸고 있고, Tooltip.Root 태그 두 개를 사용하여 두 개의 th 태그에 tooltip을 표시하고 있습니다. Tooltip.Trigger 안에는 th 태그 내용이 들어가 있고, Tooltip.Portal 안에는 tooltip에 표시될 Tooltip.Content가 있습니다. 코드가 길어질 뿐, 코드 자체는 간단합니다.


4. CSS 적용

 

  CSS를 적용하려면 CSS 파일을 생성한 후에, 공식 홈페이지에 있는 CSS 내용을 복사 붙여넣기 해주시고, css 파일을 import 해주시면 됩니다.


5. 주요 옵션

<Tooltip.Provider delayDuration={0} disableHoverableContent={false}> // 사용 예시

1. Provider

1) delayDuration (기본값: 700)

  요소 위에 마우스를 올려놨을 때, 몇 초 후에 tooltip이 표시될지 선택하는 옵션입니다. 저는 바로 뜨기를 원해서 0으로 설정했습니다.

 

2) skipDelayDuration (기본값: 300)

  tooltip 요소가 표시된 후 다른 tooltip으로 이동했을 때 설정한 delayDuration을 Reset 하는 시간을 설정합니다. 예를 들어, 학원 수에 마우스를 올려서 tooltip이 표시되었으면, skipDelayDuration에서 설정한 시간 내(300)에 다른 요소로 이동하면 바로 tooltip이 뜹니다. 하지만 skipDelayDuration에서 설정한 시간 이후에 다시 tooltip에 마우스를 올린다면, delayDuration에서 설정한 시간 후에 tooltip이 표시됩니다. skipDelayDuration을 0으로 설정한다면, 마우스를 이동할 때마다 항상 delayDuration에서 설정한 시간 후에 tooltip이 표시될 것입니다.

 

3) disableHoverableContent (Boolean 값)

  이 옵션을 false로 설정하면 tooltip 메시지 위에 마우스를 올려놓아도, tooltip이 사라지지 않지만, true로 설정하면 tooltip 메시지에 마우스를 올려놓으면 tooltip이 사라집니다. true로 설정하면 접근성에 영향을 미칠 수도 있다고 하니 기본값인 false로 설정하는 게 좋습니다.

<Tooltip.Provider delayDuration={0} disableHoverableContent={true}> // 사용 예시

2. Root

1) defaultOpen (Boolean 값)

  true로 설정하면 처음 랜더링 될 때 tooltip이 표시됩니다.

<Tooltip.Root defaultOpen={true}> // 사용 예시

3. Content

1) sideOffset (기본값: 0)

  Trigger 요소로부터 위로 얼마만큼 떨어져서 표시할 것 인지 설정합니다. 설정한 숫자가 표시할 수 있는 높이를 초과하면 아래로 표시가 됩니다.

 

2) align (기본값: 'center, ENUM = ['start', 'center', 'end']), alignOffset (기본값: 0)

  Trigger 요소로부터 왼쪽이나 오른쪽으로 얼마만큼 떨어져서 표시할 것인지 설정합니다. 기본값은 center이므로, align 값을 생략하고 alignOffset 값을 입력하면 적용이 되지 않지만, align을 'start'로 설정한 후에 alignOffset 값을 입력하면, 입력한 값만큼 오른쪽으로 표시가 됩니다. 반대로 align 값을 'end'로 설정하면 alignOffset에서 입력한 값만큼 왼쪽에서 표시가 됩니다.

<Tooltip.Content sideOffset={10} align='start' alignOffset={50}>

 

  그 외에 여러 가지 옵션들이 있지만, 기본 옵션만으로도 멋진 tooltip을 구현할 수 있기 때문에 생략하겠습니다. 필요한 옵션이 있다면 공식 문서를 참고해 주세요.


참고 문헌

1. Tooltip - Radix Primitives

2. github 예제 코드

WARNING in [eslint]
src\App.js
  Line 22:48:  Array.prototype.map() expects a return value from arrow function  array-callback-return

 

  React에서 map 함수를 사용했을 때 결과가 제대로 나오지 않으면서 "Array.prototype.map() expects a return value from arrow function"이라는 에러가 발생할 때가 있습니다. React는 JSX문법을 사용하는데, HTML 태그를 리턴해주는 경우 JavaScript에서 사용했을 때 처럼 중괄호를 이용해서 map 함수를 작성하면 위의 에러가 발생합니다.

// JavaScript
const A = [1, 2, 3, 4, 5];

A.map((data) => {
  console.log(`${data}번 째 console.log`);
  console.log(data);
});

// JSX
const Breeds = (props) => {
  return (
    <dl>
      {props.list.map(({ breed, description }) => {      // 중괄호({ })가 아닌 괄호(( ))를 써야 함
        <Fragment key={breed}>
          <dt>{breed}</dt>
          <dd>{description}</dd>
        </Fragment>;
      })}
    </dl>
  );
};

 

  JavaScript에서 map 함수를 이용해서 여러 줄을 반환할 때 중괄호를 써야 하지만, JSX에서는 중괄호를 괄호로 바꿔주어야 에러가 발생하지 않고 함수가 제대로 실행됩니다.

import { Fragment } from "react";

const Breeds = (props) => {
  return (
    <dl>
      {props.list.map(({ breed, description }) => (    // 코드 수정한 부분
        <Fragment key={breed}>
          <dt>{breed}</dt>
          <dd>{description}</dd>
        </Fragment>;
      ))}    // 코드 수정한 부분
    </dl>
  );
};

const lists = [
  { breed: "치와와", description: "작은 품종의 개." },
  { breed: "코기", description: "귀여운 품종의 개." },
  { breed: "컴벌랜드 시프도그", description: "멸종된 품종의 개." },
];

function App() {
  return <Breeds list={lists} />;
}

export default App;

 

  이렇게 코드를 수정해주면 lists의 객체들이 제대로 화면에 표시되는 것을 볼 수 있습니다. Breeds 함수의 return 부분 처럼 여러 줄의 JSX 표현식을 반환할 때는 괄호를 사용해야 하기 때문에, map 함수를 이용해서 태그를 반환해줄 때에도 중괄호가 아닌 괄호를 사용해야 합니다.

1. 블로그 관리 → 꾸미기 → 스킨 편집 → html 편집 클릭

 

2. 파일 업로드 → "추가"를 눌러서 프로필로 사용할 이미지를 업로드합니다.

3. 파일 이름이 복잡한 경우에는 이미지 파일을 우클릭 해서 링크 주소 복사를 누르면 파일명을 쉽게 가져올 수 있습니다.

3. HTML을 클릭하고 해당 부분을 찾아서 코드를 수정해줍니다.

<div class="profileImg"><img src="./images/업로드한 이미지 파일명.jpg" /></div>
<h1><a href="{##_blog_link_##}">{##_title_##}</a></h1>
<div class="desc"><pre>{##_desc_##}</pre></div>

대괄호를 넣으니까 제 블로그 정보의 link, title, 설명이 떠서 중괄호로 바꿔놨습니다. 
붙여넣기 할 때
blog_link, title, desc의 중괄호({ })를 대괄호([ ]}로 변경해주세요.

 

붙여넣기 하실 때 blog_link, title, desc의 중괄호({ })를 대괄호([ ])로 바꿔주세요. (이미지 참고)

 

4. CSS를 클릭하고 해당 부분을 찾아서 코드를 삽입해줍니다.

 

.profileImg {
  width: 250px;
  height: 250px;
  margin: 10px auto;
}

.desc pre {
  font-family: "FontAwesome";
  line-height: 25px;
  padding: 0 0 0 40px;
}

<!-- 가운데 정렬을 할 경우 -->
.desc pre {
  font-family: "FontAwesome";
  line-height: 25px;
  text-align: center;
}

 

기본적인 틀만 잡은 상태고 구체적인 값들은 취향에 맞춰서 추가하시거나 조절하시면 됩니다.

const a = "name";
const b = "Elice";

const c = { a: b };

console.log(c); // { a: 'Elice' }

 

  객체 외부에서 선언한 변수를 객체 안의 key 값으로 넣어주려고 할 때, 위와 같이 변수 이름을 입력하면 문자열로 인식해서 값이 제대로 들어가지 않습니다. 그럴 때는 대괄호([ ])를 이용해서 변수 이름을 넣어주면 됩니다.

const a = "name";
const b = "Elice";

const c = { [a]: b }; // a가 아니라 [a]를 넣어줌!

console.log(c); // { name: 'Elice' }

 

  이렇게 대괄호 안에 변수 이름을 넣어주면 위와 같이 변수의 값이 제대로 key 값으로 들어간 것을 볼 수 있습니다.


const { option, keyword } = req.query;

// 닉네임으로 검색
if (option === "nickname") {
  board = await Board.find({
    nickname: new RegExp(keyword, "i"),
  })
    .lean()
    .sort({ boardId: -1 });
}

// 제목으로 검색
if (option === "title") {
  board = await Board.find({
    title: new RegExp(keyword, "i"),
  })
    .lean()
    .sort({ boardId: -1 });
}

// 내용으로 검색
if (option === "contents") {
  board = await Board.find({
    contents: new RegExp(keyword, "i"),
  })
    .lean()
    .sort({ boardId: -1 });
}

 

  프로젝트를 하면서 게시판 검색 기능을 구현한 코드입니다. 검색 옵션과 내용을 query에서 option과 keyword 값으로 받아왔습니다. 만약에 닉네임으로 "lapras"를 검색했다면 option 값은 nickname, keyword 값은 "lapras"가 될 것입니다. 위의 코드를 보면 중복되는 부분이 많은데, 객체 안에 받아온 option 값을 바로 key 값으로 넣어주면 중복 없이 코드를 한 번만 작성하면 되지만 프로젝트 기간에는 그 방법을 몰라서 option 값에 따라서 상황마다 코드를 작성했었습니다. 

const { option, keyword } = req.query; // option 값은 nickname, title, contents

board = await Board.find({
  [option]: new RegExp(keyword, "i"),
})
  .lean()
  .sort({ boardId: -1 });

 

  이렇게 변수 값을 객체에 직접 넣어주면 불필요한 중복 없이 코드를 깔끔하게 작성할 수 있습니다. option 값으로 nickname, title, contents를 받아온 후에 key 값으로 넣어주면 if문을 작성할 필요 없이 바로 조건에 맞는 글을 찾아올 수 있습니다. 하지만 이런 방법을 쓰면 보안상 문제가 있을 수도 있으므로 option 값으로 원하는 값만 받을 수 있도록 화이트리스트를 작성하는 것이 좋습니다.

const option = 1;
const FIELD_WHITELIST = ["nickname", "title", "contents"];

if (!FIELD_WHITELIST.includes(option)) {
  throw new Error("invalid option")
}

 

  위와 같이 설정을 해주면 query에서 받아온 option 값이 화이트리스트에 없을 경우 에러가 발생합니다. 따라서, password와 같은 값들이 option으로 들어와도 검색이 되지 않습니다.

  Number와 parseInt는 모두 문자열을 숫자로 변환해 주는 함수입니다. 프로젝트를 하면서 Number를 사용한 적이 있었는데, 어떤 상황에서 Number를 써야 하고 parseInt를 써야 할지 애매할 때가 있었습니다. 그래서 Number와 parseInt의 차이점은 어떤 것이 있는지 궁금해서 자료를 정리해 봤습니다.


1. 문자열에 숫자만 있을 경우 1 - 정수

const a = "10";

console.log(Number(a)); // 10
console.log(parseInt(a)); // 10

const b = "12.00";

console.log(Number(b)); // 12
console.log(parseInt(b)); // 12

 

  문자열이 정수로만 되어있는 경우 Number와 parseInt의 결과는 동일합니다. 문자열 그대로 10을 반환합니다.

2. 문자열에 숫자만 있을 경우 2 - 실수

const a = "3.14";

console.log(Number(a)); // 3.14
console.log(parseInt(a)); // 3
console.log(parseFloat(a)); // 3.14

 

  하지만 문자열이 위와 같이 소수점으로 되어있을 경우에는 Number는 소수점까지 같이 반환하는 반면, parseInt는 소수점을 버린 정수 부분만을 반환합니다. Number와 동일한 값을 얻으려면 parseInt가 아닌 parseFloat를 사용해야 합니다.


3. 문자열에 숫자가 없을 경우

const a = "Elice";

console.log(Number(a)); // NaN
console.log(parseInt(a)); // NaN

 

  문자열에 숫자가 존재하지 않는 경우에는, Number와 parseInt 모두 NaN(Not-A-Number)을 반환합니다.

 

4. 문자열에 숫자가 섞여 있을 경우

const a = "22 Hello World";

console.log(Number(a)); // NaN
console.log(parseInt(a)); // 22

const b = "Hello 22 World";

console.log(Number(b)); // NaN
console.log(parseInt(b)); // NaN

const c = "Hello World 22";

console.log(Number(c)); // NaN
console.log(parseInt(c)); // NaN

const d = "123_456"

console.log(Number(d)); // NaN
console.log(parseInt(d)); // 123

 

  문자열 맨 앞에 숫자가 있을 경우에 Number는 NaN을 반환하지만, parseInt는 맨 앞에 있는 숫자를 반환합니다. 다만, 숫자가 문자열의 중간이나 맨 끝에 있을 경우 parseInt 역시 NaN을 반환합니다.

const a = ["123", "Elice", "텐동"];

console.log(Number(a)); // NaN
console.log(parseInt(a)); // 123

const b = ["Elice", "123", "텐동"];

console.log(Number(b)); // NaN
console.log(parseInt(b)); // NaN

 

  배열의 경우에도 마찬가지입니다. 배열의 맨 앞에 숫자로 된 문자열이 있다면 parseInt는 숫자로 변환을 합니다. 하지만 맨 앞이 아니라면 숫자를 인식하지 못하고 NaN을 반환합니다.

const a = ["123", "456", "Elice", "탕후루"];

console.log(Number(a)); // NaN
console.log(parseInt(a)); // 123

 

  만약 0번, 1번 index에 숫자로 된 문자열이 있으면 어떻게 될까요? parseInt는 0번 index에 있는 문자열만 숫자로 반환해 줍니다.


5. null, undefined, true, false

const a = null; // const a = " " 결과는 같음

console.log(Number(a)); // 0
console.log(parseInt(a)); // NaN

const b = undefined;

console.log(Number(b)); // NaN
console.log(parseInt(b)); // NaN

const c = true;

console.log(Number(c)); // 1
console.log(parseInt(c)); // NaN

const d = false;

console.log(Number(d)); // 0
console.log(parseInt(d)); // NaN

 

  이 경우에 parseInt는 모두 NaN을 반환합니다. 하지만 Number는 null과 false일 때 0을, true일 때 1을 반환합니다. 문자열이 ""뿐만 아니라 "     "처럼 공백만 있어도 Number는 0을 반환합니다.

 

6. Infinity, -Infinity

const a = Infinity;

console.log(Number(a)); // Infinity
console.log(parseInt(a)); // NaN

const b = -Infinity;

console.log(Number(b)); // -Infinity
console.log(parseInt(b)); // NaN

 

  이 경우에도 parseInt는 NaN을 반환하지만, Number는 무한대일 때 무한대 값을 그대로 반환합니다.


7. 문자열이 특정 진수일 때

const a = "0x11"; // 17의 16진수 값

console.log(Number(a)); // 17
console.log(parseInt(a)); // 17, parseInt는 radix를 지정해주지 않으면 16진수와 10진수만 구별함

const b = "0o11"; // 9의 8진수 값

console.log(Number(b)); // 9
console.log(parseInt(b)); // 0

const c = "0b11"; // 3의 2진수 값

console.log(Number(c)); // 3
console.log(parseInt(c)); // 0

 

  parseInt는 parseInt(string, radix)처럼 두 번째 매개변수로 진수 값을 지정해 줄 수 있습니다. 만약 radix를 생략하고 string만 매개변수로 지정해 준다면 "0x"로 시작할 때는 자동으로 16진수로 변환해 주지만, "0o"나 "0b"와 같은 8진수와 2진수 값은 인식하지 못하고 앞에서 본 것처럼 문자의 맨 앞 숫자인 0을 반환해 줍니다. 하지만 Number의 경우에는 "0x", "0o", "0b"가 앞에 있으면 자동으로 16진수, 8진수, 2진수의 숫자로 변환해 줍니다. (ECMAScript 5 이후부터 parseInt를 이용해서 0으로 시작하는 문자열을 8진수로 해석하는 것을 금지함)

// parseInt(string, radix)
parseInt(11, 16) // 17
parseInt(11, 8) // 9
parseInt(11, 2) // 3
parseInt(11, 10) // 11

parseInt("0xF") // 15, radix가 없어도 "0x"로 시작하면 16진수로 변환
parseInt("0xF", 16) // 15
parseInt("F", 16) // 15

parseInt("Elice", 8) // NaN
parseInt("546", 2) // NaN, 546은 2진수가 아님

 

  parseInt는 매개변수로 파싱 할 값인 string과, string의 진수를 나타내는 radix를 매개변수로 받을 수 있습니다. 위에서 언급한 것처럼 radix값을 생략하면 문자열에 "0x"가 존재할 경우에는 16진수로, 없을 경우에는 10진수로 자동으로 변환해 줍니다. radix는 2부터 36까지의 정수입니다. 만약 radix를 설정했는데 string에 유효하지 않은 문자열이 들어갔다면 NaN을 반환합니다. 


8. 지수(e)가 포함된 문자열

const a = "1e+20";

console.log(Number(a)); // 100000000000000000000
console.log(parseInt(a)); // 1

const b = "1e+21";

console.log(Number(b)); // 1e+21, 이상이면 지수로 표현
console.log(parseInt(b)); // 1

const c = "1e-6";

console.log(Number(c)); // 0.000001
console.log(parseInt(c)); // 1

const d = "1e-7";

console.log(Number(d)); // 1e-7, 이하면 지수로 표현
console.log(parseInt(d)); // 1

 

  Number는 지수(e)를 인식해서 숫자로 잘 변환해 주는 반면, parseInt는 e를 문자열로 인식하기 때문에 문자열 앞에 있는 숫자만 변환해 줍니다. 만약 "123e+2"의 값이 주어진다면 Number는 12300을, parseInt는 e 앞의 숫자인 123을 반환합니다. 참고로 위의 예제처럼 숫자가 1e+21을 초과하거나, 1e-7 미만인 경우에는 지수가 붙은 값을 그대로 반환하고, 그 사이의 숫자는 지수만큼 0을 붙여서 반환합니다.

const a = "1000000000000000000000";

console.log(Number(a)); // 1e+21
console.log(parseInt(a)); // 1e+21

const b = "0.0000001";

console.log(Number(b)); // 1e-7
console.log(parseInt(b)); // 0
console.log(parseFloat(b)); // 1e-7

 

  반대로 1e+21이 아닌 1e+1의 값을 풀어서 입력했다면 결과값은 Number와 parseInt 모두 동일하게 1e+21로 나옵니다. 1e-7의 값을 풀어서 입력했다면 Number는 그대로 1e-7을 반환하지만, parseInt는 실수는 값을 버리고 반환하기 때문에 0을 반환합니다. Number와 같은 결과값을 얻으려면 parseFloat를 이용해야 합니다.


9. Date 객체를 숫자로 변환

const a = new Date("May 17, 2024 12:13:00");

console.log(Number(a)); // 1715915580000
console.log(parseInt(a)); // NaN

 

  Date 객체를 숫자로 변환할 때는 Number를 사용합니다. parseInt는 문자열의 처음이 숫자가 아니므로 NaN을 반환합니다.


10. Map 함수를 이용해서 배열의 문자열을 숫자로 변환

const a = ["1", "2", "3", "4", "5"];

const b = a.map(Number);
const c = a.map(parseInt);

console.log(b); // [ 1, 2, 3, 4, 5 ]
console.log(c); // [ 1, NaN, NaN, NaN, NaN ]

 

  map함수를 이용해서 객체의 문자열을 하나씩 숫자로 변환하려고 할 때, Number를 이용하면 객체 안의 모든 원소를 숫자로 변환해 주지만, parseInt를 사용하면 첫 번째 원소만 숫자로 변환하고 나머지는 NaN을 반환해 줍니다.

const a = ["1", "2", "3", "4", "5"];

for (i = 0; i < a.length; i++) {
  a[i] = Number(a[i]);
}

console.log(a); // [ 1, 2, 3, 4, 5 ]

const b = ["1", "2", "3", "4", "5"];

for (i = 0; i < b.length; i++) {
  b[i] = parseInt(b[i]);
}

console.log(b); // [ 1, 2, 3, 4, 5 ]

 

  for문을 이용했을 때는 Number와 parseInt 모두 문자열을 숫자로 잘 변환해 줍니다. 따라서 map함수를 이용할 때는 Number를 사용하고, for문을 이용할 때는 Number와 parseInt 모두 사용해도 같은 결과를 얻을 수 있습니다.

 


11. 성능 

function benchmark(name, cb) {
  const t0 = performance.now();
  for (let i = 0; i < 123e+6; i++) {
    cb();
  }
  const t1 = performance.now();
  console.log(`${name} took ${t1 - t0} ms`);
}

// 첫 번째 테스트
const process1 = () => Number("10");
const process2 = () => parseInt("10");
const process3 = () => Number("12.00");
const process4 = () => parseInt("12.00");
const process5 = () => parseFloat("12.00");

benchmark("process4", process4); // 49.046119000000004 ms  (제외)
benchmark("process1", process1); // 504.40277 ms
benchmark("process2", process2); // 497.77029400000004 ms
benchmark("process3", process3); // 492.64266900000007 ms
benchmark("process4", process4); // 493.3410110000002 ms
benchmark("process5", process5); // 4398.32734 ms  (parseFloat)

// 두 번째 테스트
const process1 = () => Number("22 Hello World");
const process2 = () => parseInt("22 Hello World");
const process3 = () => Number("Hello 22 World");
const process4 = () => parseInt("Hello 22 World");

benchmark("process4", process4); // 57.123753 ms  (제외)
benchmark("process1", process1); // 516.625815 ms
benchmark("process2", process2); // 493.05675900000006 ms
benchmark("process3", process3); // 492.7396140000001 ms
benchmark("process4", process4); // 489.58944299999985 ms

// 세 번째 테스트
const process1 = () => Number(null);
const process2 = () => parseInt(null);
const process3 = () => Number(undefined);
const process4 = () => parseInt(undefined);
const process5 = () => Number(true);
const process6 = () => parseInt(true);
const process7 = () => Number(false);
const process8 = () => parseInt(false);

benchmark("process4", process4); // 5698.4390539999995 ms  (제외)
benchmark("process1", process1); // 500.9703870000003 ms
benchmark("process2", process2); // 6288.984187 ms  (parseInt)
benchmark("process3", process3); // 490.3260759999994 ms
benchmark("process4", process4); // 6290.612854000001 ms  (parseInt)
benchmark("process5", process5); // 501.7009740000003 ms
benchmark("process6", process6); // 6348.691674000001 ms  (parseInt)
benchmark("process7", process7); // 492.38502999999946 ms
benchmark("process8", process8); // 6385.050194999998 ms  (parseInt)

// 네 번째 테스트
const process1 = () => Number("1000000000000000000000");
const process2 = () => parseInt("1000000000000000000000");
const process3 = () => Number("0.0000001");
const process4 = () => parseInt("0.0000001");

benchmark("process4", process4); // 49.753349 ms  (제외)
benchmark("process1", process1); // 517.162016 ms
benchmark("process2", process2); // 9484.792979 ms  (parseInt)
benchmark("process3", process3); // 497.1809140000005 ms
benchmark("process4", process4); // 498.4548539999996 ms

 

  벤치마크 함수를 이용해서 두 함수의 성능을 비교해 봤습니다. 실행 전 시간과 123e+6번 함수를 실행한 후의 시간을 비교해서 실행 시간을 반환해 줍니다. 첫 번째 벤치마크를 실행하면 시간이 다른 벤치마크 시간과 차이가 많이 나기 때문에, 첫 번째 결과는 제외하고 나머지 시간들을 비교해 봤습니다. 일반적인 상황에서 두 함수의 실행 시간은 거의 비슷했습니다. 하지만 null, undefined, true, false와 같은 값들을 변환할 때, Number는 다른 값들을 변환할 때와 시간 차이가 없었던 반면 parseInt는 느린 성능을 보여줬습니다. 또한 1e+20까지는 Number와 parseInt가 비슷한 성능을 보여줬지만, 1e+21이 넘어가는 순간 parseInt의 성능이 현저히 느려졌습니다. 물론 일반적인 상황에서는 이렇게까지 큰 숫자를 변환할 일이 없을 것입니다. 결과적으로 성능만 봤을 때는 parseInt보다는 Number가 좋았습니다.


 

12. 정리

  두 함수의 차이를 다시 정리해 보겠습니다.

  Number parseInt
정수 ( a = "10" ) 10 10
실수 ( a = "3.14" ) 3.14 3
문자열 ( a = "Elice" )
숫자가 있는 문자열 ( a = "22 Elice")
NaN
NaN
NaN
22 (맨 앞의 숫자만)
null (+공백), undefined, true, false
Infinity, -Infinity
0, NaN, 1, 0
Infinity, -Infinity
NaN
NaN
진수 표현 16진수(0x11), 8진수(0o11), 2진수(0b11) "0x"일 때 16진수, 없으면 10진수
( 매개변수 radix로 진수 지정 가능 )
지수(e) ( a = 1e+21 ) 1e+21 1 (e 앞의 숫자만)
Date ( "May 17, 2024 12:13:00" ) 1715915580000 NaN
Map 함수 ( a = [ "1" , "2" , "3" , "4" , "5" ] ) [ 1, 2, 3, 4, 5 ] [ 1, NaN, NaN, NaN, NaN ]
성능 빠름 비슷하지만 일부 항목에서 느림 
( null, undefined, true, false, 1e+21 )  

 

  정리해 보면 대부분 일반적인 상황에서는 Number를 사용하는 것이 좋아 보입니다. 

* parseInt를 사용하는 경우

1. 실수를 정수로 변환하고 싶을 때 ("3.14" => 3)
2. 변환해야 할 문자열 앞에 숫자가 있을 때 ("22 Hello World" => 22)
3. Number에서 지원하지 않는 진수를 써야할 때 (radix 값은 2에서 36까지 가능)
4. 0이나 1이 의미를 갖는 경우 (null, "", true, false)

 

  다만 위와 같은 경우처럼 parseInt를 사용하는 경우도 있으므로, 상황에 맞게 Number와 parseInt를 선택하면 될 것 같습니다.


const boardIdList = [];
for (const item of searchedComments) {
  boardIdList.push(Number(item.boardId));
}
const boardIdSet = [...new Set(boardIdList)].sort((a, b) => b - a);

 

  위 코드는 프로젝트를 하면서 작성한 게시판 댓글 검색 코드입니다. DB의 Comment 자료에는 댓글이 달린 글의 boardId를 담고 있습니다. 예를 들면 2번 글의 첫 번째 댓글이라면 DB에는 { boardId: 2, commentId: 1 } 이런 식으로 저장이 됩니다. 댓글 내용으로 검색을 하지만 검색 결과는 게시글을 보여줘야 하므로, 검색된 댓글의 boardId를 추출해야 합니다. 추출한 boardId값은 string으로 되어있어서 Number 함수를 이용해 숫자로 변환을 한 후에 배열에 넣어주었습니다. 한 글에서 여러 개의 댓글이 검색될 수도 있기 때문에 boardId가 중복될 수도 있어서 Set함수를 이용해 boardId의 중복을 제거하고 정렬을 해주었습니다.

  위 코드에 나오지는 않지만 mongoose sequence plugin을 사용하여 boardId가 1, 2, 3 이런 식으로 자동으로 부여가 되기 때문에, 만약 boardId에 잘못된 값이 들어가서 boardId를 1로 반환해 준다면 boardId가 1인 글이 검색 결과에 표시될 수도 있습니다. 그렇기 때문에 이런 상황에서는 Number 보다는 parseInt를 사용하는 것이 좋을 것 같습니다.


참고 문헌

1. parseInt() - MDN 문서

2. Number - MDN 문서

3. Number.parseInt() vs. parseInt() - reddit.com

4. parseInt() vs Number() - Emre Avcılar

  JavaScript 공부를 하면서 난감한 경우 중 하나가 아래와 같이 물음표(?)와 같은 특수 문자를 마주할 때입니다.

const dogName = adventurer.dog?.name;

 

  구글에서 'JavaScript ?.' 으로 검색을 하면 'JavaScript' 검색 결과만 나와서 이걸 어떻게 찾아야 할지 막막했던 기억이 납니다. 그래서 이번에는 JavaScript에서 물음표를 사용하는 연산자들을 정리해 보도록 하겠습니다.


1. 조건 (삼항) 연산자 (A ? B : C) - Conditional (ternary) operator

(조건) ? (조건이 True일 때) : (조건이 False일 때)

 

  삼항 연산자는 if문과 비슷하지만, if문을 쓸 수 없을 때나 간결하게 쓸 수 있을 때 많이 사용합니다.

const age = 26;
const beverage = age >= 21 ? "Beer" : "Juice";
console.log(beverage); // "Beer"
// -----------------------------------------------------
const age = 17;
const beverage = age >= 21 ? "Beer" : "Juice";
console.log(beverage); // "Juice"

 

  위의 예시를 보면 age가 21 이상인 경우에는 'Beer', 그 외에는 'Juice'가 beverage의 값이 됩니다. age가 26일 경우에 console.log를 찍어보면 true일 때 값인 "Beer"가, age가 17일 경우에는 false일 때의 값인 "Juice"가 표시됩니다. 

if (age >= 21) {
  return "Beer";
} else {
  return "Juice";
}

 

  if문으로 표현하면 여러 줄이 필요하기 때문에, if문이 복잡하지 않은 경우에는 삼항 연산자를 쓰는 편이 깔끔해 보입니다.

<main>
  <h1>Welcome to this website</h1>
  {isLoggedIn ? (
    <button onClick={logout}>Log out</button>
  ) : (
    <button onClick={login}>Log in</button>
  )}
</main>

 

  다음으로는 if문을 사용할 수 없는 경우입니다. React에서 사용하는 경우가 많습니다. 위의 예시에서는 로그인이 되어있으면 isLoggedIn 값을 true로 설정해 줍니다. 따라서 로그인이 되어 있는 경우에는 로그아웃 버튼이 보이고, 로그아웃이 되어있는 경우에는 isLoggedIn 값이 false이므로, 로그인 버튼이 보이게 됩니다.

<button onClick={isPlaying ? onClickPause : onClickPlay}>
  {isPlaying ? "Pause" : "Play"}
</button>

 

  위의 예시에서도 동영상이 재생 중인 경우에는 isPlaying 값이 true로 설정되어 있습니다. 따라서 재생 중일 때는 버튼의 이름이 "Pause"가 되고 버튼을 클릭하면 동영상을 멈추는 onClickPause 함수가 실행됩니다. 반대로 동영상이 멈춰있는 경우에 isPlaying 값이 false가 되어서, 버튼 이름은 "Play"가 되고 버튼을 클릭하면  동영상을 재생해 주는 onClickPlay 함수가 실행됩니다. 

<main>
  <h2 style={{ display: "flex", gap: "6px" }}>
    Secret password
    <button onClick={() => setExpended(false)}>-</button>
    <button onClick={() => setExpended(true)}>+</button>
  </h2>
  {isExpended && (
    <p>
      password: <code>hunter2</code>
    </p>
  )}
</main>;

 

  참고로 AND(&&) 연산자 사용하면 연산자 앞의 값이 True일 때, 연산자 뒤의 값을 리턴해줍니다. + 버튼을 누르면 isExpended값이 true가 되면서 <p> 태그 안에 있는 password의 값이 보이게 됩니다.


2. 널 병합 연산자 (??) - Nullish coalescing operator

a ?? b // a 값이 null, undefined면 b를 반환함

 

  물음표 두 개(??)는 Nullish coalescing operator입니다. ?? 앞의 값이 null 값이거나 undefined면 뒤의 값을 반환해줍니다.

const firstName = null;
const lastName = undefined;
const nickName = "Violet";

alert(firstName ?? lastName ?? nickName ?? "Cheese"); // Violet

 

  firstName 값이 null이고, lastName 값은 undefined이므로, 뒤에 있는 nickname 값이 반환됩니다. 만약 nickName도 null이거나 undefined였다면, 뒤에 있는 "cheese" 값을 반환해 줄 것입니다.

1. Nullish coalescing operator 
  -> 첫 번째 정의된(defined) 값을 반환
  -> null, undefined 값만 취급한다.
2. OR(||) 연산자
  -> 첫 번째 truthy 값을 반환
  -> null, undefined 외에도 0 값을 falsy 값으로 취급한다.

 

  앞에서 AND(&&) 연산자가 사용된 경우를 봤다면, 이번에는 OR(||) 연산자가 사용된 경우를 보겠습니다. Nullish coalescing operator는 || 연산자와 유사하지만, 차이점이 존재합니다. 전자의 경우에는 첫 번째 정의된 값을 반환하고, null과 undefined 값만 취급합니다. 후자의 경우에는 첫 번째 truthy 값을 반환하고, null과 undefined 외에도 0 값을 falsy 값으로 취급합니다. 따라서, 숫자 0이 할당될 수 있는 변수를 사용할 때는 ||보다는 ??을 사용하는 것이 좋습니다.

let height = 0;

alert(height || 100); // 100
alert(height ?? 100); // 0

 

  height 값을 0으로 설정하고 || 연산자를 사용하는 경우에는, 0을 falsy 값으로 취급하므로 첫 번째 truthy 값인 100을 반환합니다. ?? 연산자를 사용하는 경우에는 0은 truthy 값이 되어 null이나 undefined 값이 아니게 됩니다. 따라서 첫 번째 정의된 값인 0을 반환해 줍니다. 위의 예시처럼 0이 의미가 있는 값이라면 ?? 연산자를 사용하는 것이 적합합니다. 


3. 옵셔널 체이닝 (?.) - Optional chaining

let user = {}; // 주소 정보가 없는 사용자

alert( user?.address?.street ); // undefined, 에러가 발생하지 않습니다.

// Nullish coalescing operator를 사용할 경우
alert( user && user.address && user.address.street ); // undefined, 에러가 발생하지 않습니다.

 

  Optional Chaining은 ?. 앞의 값이 "있다.", "없다."만을 평가합니다. 만약 ?. 앞의 값이 null이나 undefined인 경우에는 평가를 멈추고 undefined를 반환합니다. 에러는 발생시키지 않습니다. Nullish coalescing operator와 유사하지만, Optional Chaning을 사용하면 코드를 더 간결하게 작성할 수 있습니다. 위의 예시에서는 user는 존재하지만, user의 address 항목이 존재하지 않기 때문에 undefined를 반환할 것입니다.

const hashedPassword = await User.findOne({ userId }, "password")
  .lean()
  .then((user) => user?.password);

 

  제가 프로젝트 하면서 작성했던 코드입니다. findOne함수를 이용해 DB에서 userId가 일치하는 user의 password를 가져왔습니다.

{
  _id: new ObjectId('684e4ccc91757a381b66e7ad'),
  password: '$2b$10$sXCscjG5Wn6FKBEfn.lwF.NOVyAO/6EP5HDO.OBgn9GbXXCVzjIZS'
}

 

  만약 user가 존재한다면 위와 같이 _id와 password 필드를 가져올 것입니다. user가 존재하지 않는다면 user?.password는 undefined 값을 반환할 것입니다. 이 예시에는 user가 존재하므로, 객체의 password 값을 아래와 같이 반환합니다.

$2b$10$sXCscjG5Wn6FKBEfn.lwF.NOVyAO/6EP5HDO.OBgn9GbXXCVzjIZS

참고 문헌

1. Conditional (ternary) operator - MDN 문서

2. Nullish coalescing operator - MDN 문서

3. Nullish coalescing operator - JAVASCRIPT.INFO

4. Optional chaining - MDN 문서

5. Optional chaining - JAVASCRIPT.INFO

+ Recent posts