728x90

웹 접근성이란  장애인, 고령자 등이 웹 사이트에서 제공하는 정보에 비장애인과 동등하게 접근하고 이해할 수 있도록 보장하는 것입니다.

 

이를 향상시키기 위해서 시멘틱한 코드를 작성할 필요가 있는데, 단순히 alt를 적고 aria 태그를 이용하고 시멘틱 태그를 사용한다고 해서 과연 적절하게 작성했는지에 대해서는 확신할 수 없습니다.

 

따라서 저는 NVDA의 Screen Reader를 통해서 시각 장애인이 실제 내 사이트에 접근했을 때 어떻게 읽을지에 대해서 확인해 보았습니다.

 

 

사이트는 간단하게 학원 주소 이미지, 학원 이름, 오시는 길, 연락처등이 있는 페이지입니다.

(상호가 일점육수학과학전문학원이라서 검색에 불편함을 겪는 학부모님들을 위한 사이트)

 

사이트 기능이 단순한 만큼 Screen Reader로 읽었을 때 큰 문제가 없을거라 생각했는데,  2가지 불편함을 확인하여 이를 개선하였습니다.

 

#1. --> 으로 인한 불편함

오시는 길 옆에 화살표는 마우스를 호버했을 때 버튼이라는 시각적 효과를 주기 위해서 삽입하였습니다.

하지만 이를 스크린리더가 읽게 되면

오시는 길 언더바 언더바 그레이터댄 이라고 읽습니

따라서 불필요한 내용에 대해서 Screen Reader가 읽지 않도록 설정할 필요성이 있습니다.

이때 사용하는 것이 aria-hidden 속성입니다.

   <span
        className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none"
        aria-hidden="true"
      >

 

이후 위의 연락처 -> 052-261- 5515를 Screen Reader에 읽게 하면

연락처 052 언더바 261 언더바 5515 버튼 이라고 읽게 됩니다.

 

(Children Presentational이라고 자식요소를 한꺼번에 읽은 후 자신의 역할을 설명하는 요소들이 있습니다.

이때 버튼도 해당 요소중 하나라서 위와 같이 읽습니다.)

https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion


#2. 연락처 버튼을 클릭시 번호가 클립보드에 저장되는 이를 시각장애자 입장에서 알기 힘들다 

function CardClipWrapper({ text, children }: CardClip) {
  const copylink = () => {
    navigator.clipboard.writeText(text);
    alert(text + '가 클립보드에 복사되었습니다');
  };
  return (
    <button
      onClick={copylink}
      className="flex flex-col items-start lg:text-left group rounded-lg border h-full px-5 py-4 transition-color border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30 w-full max-w-96"
    >
      <span className="sr-only">클릭시 번호가 클립보드에 저장됩니다</span>
      {children}
    </button>
  );
}

이를 위해 sr-only class를 활용하여 스크린 리더만 읽는 text를 추가하였습니다.

 

 

참고문헌

추가적인 학습을 하고 싶다면 FE 컴프 영상을 참고하거나, 

https://www.youtube.com/watch?v=tKj3xsXy9KM

 

http://www.websoul.co.kr/accessibility/WA_guide22.asp

 

웹 접근성 지침 2.2 | Web Soul Lab

웹 접근성 지침 2.2 한국형 웹 콘텐츠 접근성 지침 2.2은 4가지 원칙과 각 원칙을 준수하기 위한 14개 지침 및 해당 지침의 준수여부를 확인하기 위해 33개의 검사항목으로 구성되어 있습니다. ※ 20

www.websoul.co.kr

에서 내용을 확인해도 좋다고 생각합니다.

728x90

프로젝트 진행 배경

 

아버지의 학원 이름이 1.6수학과학학원인데, 상표등록시에 숫자를 사용할 수 없어서 일점육수학과학학원으로 상표등록이 되어있습니다.

이를 해결하기 위해서 간단한 웹피이지를 만들어서 검색엔진에 노출시키는 프로젝트를 진행하게 되었습니다. (네이버 지도에서도 1.6수학과학이라고 치면 나오지 않습니다.)

 

프로젝트 목표

1.6수학과학학원, 일점육수학과학원으로 검색했을 때, 검색엔진이 찾을 수 있도록 하는것이 목표입니다. 이때 metadata중에서 keyword는 최근에 악용되는 사례가 많아서 지원하지 않는 엔진이 많다고 합니다.(ex구글) 따라서 시멘틱 태그 및 다른 메타태그를 적극적으로 사용하여 검색엔진에 노출되도록 하려고 합니다.

 

메타 데이터 작성

이 중에서 간단하게 metadata를 우선적으로 적용하였습니다.

 

export const metadata: Metadata = {
  title: '1.6 수학과학전문학원',
  description:
    '울산 남구 옥동 1.6 수학과학전문학원입니다. 공식상호는 일점육수학과학전문학원이라 네이버지도에서는 해당 이름으로 검색해야 합니다.',
  openGraph: {
    title: '1.6 수학과학전문학원',
    description:
      '울산 남구 옥동 1.6수학과학전문학원(일점육수학과학전문학원)입니다.',
    images:
      'https://github.com/suhong99/1.6math/assets/120103909/205269b2-3969-4afd-8477-686a46aa76c8',
    locale: 'ko_KR',
    url: 'https://1-6math.vercel.app/',
    type: 'website',
    siteName: '1.6 수학과학전문학원',
  },
};

 

간단하게 학원 주소와 연락처만 노출시키는 사이트이기에 최적화할 부분이 적었고 lighthouse가 만점을 줘버렸습니다.

(동적인 요소가 적기 때문에,,)

 

 

하지만 문제점이 있습니다.

 

 구글과 네이버에서 검색해도 해당 사이트가 검색되지 않고 있습니다

 

 

검색엔진에 노출시키기

Robot.txt 작성

1번은 github 에 있는 asset이미지를 사용해서 그렇기에 해당 이미지를 cloudinary에 배포를 하면 해결할 수 있습니다.

2번에 대해서 알아보기 위해서, 아래 블로그 글을 참고하게 되었습니다.

 

https://velog.io/@rageboom/SEO-%EC%A0%81%EC%9A%A9

 

[SEO] 적용

앞에서 SEO는 어떻게 적용되고 엔지니어링에는 어떤 요소가 있는지 확인 했으니 간단한 테스트베드를 구축하고 실제 잘 동작하는지 확인해 보려 합니다.적용 순서를 정하고 한 단계 식 진행하고

velog.io

해당 링크를 통해서

https://search.google.com/search-console/about

 

Google Search Console

Search Console 도구와 보고서를 사용하면 사이트의 검색 트래픽 및 실적을 측정하고, 문제를 해결하며, Google 검색결과에서 사이트가 돋보이게 할 수 있습니다.

search.google.com

https://searchadvisor.naver.com/

 

네이버 서치어드바이저

네이버 서치어드바이저와 함께 당신의 웹사이트를 성장시켜보세요

searchadvisor.naver.com

 

에서 각각 검색엔진 진단을 할 수 있다는 점을 알게 되었습니다.

제 경우에는 robots.txt 파일이 존재하지 않았습니다. 이를 아래와 같이 작성하였고, 다시 진단해보니

 

import { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
    },
  };
}

 

 

하지만 여전히 검색엔진에 노출되지 않았습니다

 

위 사이트를 보면 알 수 있듯이. robos.txt가 있으면 SEO 최적화에 도움되지만 검색엔진 노출에 필수적인 요소는 아님은 알 수 있습니다.

 

sitemap.xml 작성

그리고 페이지가 여러 장인 경우에는 sitemap을 작성하면, 검색엔진이 각 사이트의 우선순위를 파악하는데 도움이 된다고 합니다. 비록 한 페이지지만 sitemap도 같이 작성했습니다.

https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap

 

Metadata Files: sitemap.xml | Next.js

API Reference for the sitemap.xml file.

nextjs.org

공식문서를 참고하였습니다

import { MetadataRoute } from 'next';

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://1-6math.vercel.app/',
      lastModified: new Date(),
      priority: 1,
    },
  ];
}

 

사이트 소유 확인

각각의 검색사이트에서는 site:사이트URL 을 통해서 노출여부를 확인할 수 있습니다.

이때 구글 콘솔에서 확인해 보니 검색엔진에 등록하는 작업이 필요했습니다.

 

 

네이버의 경우에는 웹마스터 도구를 이용해서 해당 url을 등록하면 관련 Html파일을 제공해주고, 구글은 도메인 및 URL 접두어를 이용하여 소유확인이 가능합니다.

이때 vercel 무료 배포의 경우네는 custom domain이 아니기에 URL접두어 방식을 이용해야 합니다.

관련 html 파일은 public 경로에 등록하게 되면 수집 요청을 할 수 있습니다

구글

 

네이버

 

각각의 검색엔진 모두 수집요청을 해야지 노출이 되는데,, 며칠 걸린다는 내용들에 기다리다가(기다릴겸 자격증 공부)하다가 오래 걸렸습니다.

 

설명글이 좀 .. 어색해서 수정해서 다시 요청중입니다... 

하지만 학부모님이 해당 사이트를 봤을 때, 검색 순위가 높지도 않고 , 내용이 부실해서 진짜 사이트가 맞는지 오해할 수 있다고 생각했습니다.

이를 해결하기 위해서 schema.org를 사용해보려고 합니다.

관련 결과는 노출 완료시 다시 포스팅하겠습니다.  schema를 잘 적용하면 아래와 같이 사이트를 좀 더 그럴듯하게 보이게 할 수 잇습니다.

 

 

728x90

캐러셀을 통해서 이미지를 보여주는 경우, 처음부터 모든 이미지를 로드 한다면 초기 로딩 속도가 느려질 것이고, 이는 사용자 경험을 하락시킨다.

이떄 react-lay-load-image-component를 활용하여 화면에 보여주는 이미지만 우선적으로 로드한다면 우리 앱의 성능을 향상시킬 수 있다.

 

 

https://www.npmjs.com/package/react-lazy-load-image-component

 

아래는 Swiper 라이브러리를 통해서 구현한 호텔 캐러셀이다. 이를 기존에 img태그를 사용하던 것을 LayLoadImage로 교체하면 화면에 보이는 이미지만 우선적으로 로드한다.

<Swiper css={containerStyles} spaceBetween={8}>
        {images.map((imageUrl, idx) => (
          <SwiperSlide key={imageUrl}>
            <LazyLoadImage
              src={imageUrl}
              alt={`${idx + 1}번째 호텔의 이미지`}
              css={imageStyles}
              height={300}
            />
            {/* <img
              src={imageUrl}
              alt={`${idx + 1}번째 호텔의 이미지`}
              css={imageStyles}
              height={300}
            /> */}
          </SwiperSlide>
        ))}
      </Swiper>

 

위 코드 적용시

요소탭에서 이미지를 처음에 보여줄 2개만 우선적으로 받느 것을 확인할 수 있고, 성능 탭을 통하여서, 이미지를 더 빠르게 로드하는 것을 확인할 수 있다.

 

 

 

이후 캐러셀을 넘길 때마다 추가적으로 네트워크에서 이미지 요청한다느 사실을 확인할 수 있다.

728x90

리엑트는 SPA이기 때문에, 기본적으로 MPA와 달리 각각의 페이지에 대한 메타데이터가 처리되지 않는다.

이를 react-helmet-async 라이브러리를 사용하여 수월하게 할 수 있다. 이러한 메타데이터가 올바르게 처리되어야지 우리 앱이 노출될 가능성이 올라간다.

 

아래와 같이 우리 앱을 HelmetProvider로 감싸고, Helmet 태그에 지정하고 싶은 타이틀이나 메타데이터들을 작성하면 된다.

import React from 'react';
import ReactDOM from 'react-dom';
import { Helmet, HelmetProvider } from 'react-helmet-async';

const app = (
  <HelmetProvider>
    <App>
      <Helmet>
        <title>Hello World</title>
        <link rel="canonical" href="https://www.tacobell.com/" />
      </Helmet>
      <h1>Hello World</h1>
    </App>
  </HelmetProvider>
);

ReactDOM.hydrate(
  app,
  document.getElementById(‘app’)
);

https://www.npmjs.com/package/react-helmet-async

 

react-helmet-async

Thread-safe Helmet for React 16+ and friends. Latest version: 2.0.4, last published: 25 days ago. Start using react-helmet-async in your project by running `npm i react-helmet-async`. There are 547 other projects in the npm registry using react-helmet-asyn

www.npmjs.com

 

이제 실제 프로젝트에서 이를 적용시켜보려고 한다.

CRA로 앱을 빌드하게 되면 기본적인 타이틀은  React App이다.

이를 수정하려면 index.html 파일에서 작성할 수 있다.

 <title>LoveTrip</title>
    <meta name="description" content="여행의 시작은 LoveTrip 에서" />
    <meta property="og:type" content="website" />
    <meta property="og:title" content="LoveTrip" />
    <meta
      property="og:image"
      content="https://www.touropia.com/gfx/b/2015/05/osaka.jpg"
    />
    <meta property="og:image:width" content="260" />
    <meta property="og:image:height" content="260" />
    <meta property="og:description" content="여행의 시작은 LoveTrip 에서" />
    <meta property="og:locale" content="ko_KR" />
  </head>

 

이렇게 등록되면 우리 앱은 우선적으로 메타데이터가 적용은 되지만 이는 모든 페이지에서 똑같은 데이터를 준다.

이를 공식 문서와 같이 우리 앱을 HelmetProvider로 감싸주고,

 <HelmetProvider>
        <BrowserRouter>
          <AuthGuard>
            <Navbar />
            <Routes>
              <Route path="/" element={<HotelListPage />} />
              <Route
                path="/my"
                element={
                  <PrivateRoute>
                    <MyPage />
                  </PrivateRoute>
                }
              />
              <Route path="/signin" element={<SigninPage />} />
              <Route path="/test" element={<TestPage />} />
              <Route path="/hotel/:id" element={<HotelPage />} />
              <Route
                path="/settings"
                element={
                  <PrivateRoute>
                    <SettingsPage />
                  </PrivateRoute>
                }
              />
              <Route
                path="/settings/like"
                element={
                  <PrivateRoute>
                    <LikePage />
                  </PrivateRoute>
                }
              />
              <Route
                path="/schedule"
                element={
                  <PrivateRoute>
                    <SchedulePage />
                  </PrivateRoute>
                }
              />
              <Route
                path="/reservation"
                element={
                  <PrivateRoute>
                    <ReservationPage />
                  </PrivateRoute>
                }
              />
              <Route
                path="/reservation/done"
                element={
                  <PrivateRoute>
                    <ReservationDonePage />
                  </PrivateRoute>
                }
              />
              <Route
                path="/reservation/list"
                element={
                  <PrivateRoute>
                    <ReservationListPage />
                  </PrivateRoute>
                }
              />
            </Routes>
          </AuthGuard>
        </BrowserRouter>
      </HelmetProvider>

 

동적으로 SEO를 적용해주는 컴퍼넌트를 생성한 후에

import { Helmet } from 'react-helmet-async'

interface SEOProps {
  title: string
  description: string
  image: string
}

function SEO({ title, description, image }: SEOProps) {
  return (
    <Helmet>
      <title>{title}</title>
      <meta name="description" content={description} />
      <meta property="og:type" content="website" />
      <meta property="og:title" content={title} />
      <meta property="og:image" content={image} />
      <meta property="og:image:width" content="260" />
      <meta property="og:image:height" content="260" />
      <meta property="og:description" content={description} />
      <meta property="og:locale" content="ko_KR" />
    </Helmet>
  )
}

export default SEO

 

적용 하고자 하는 페이지에 적용시키면 된다. 

      <SEO title={name} description={comment} image={images[0]} />

 

이럴 경우 TITLE이 수정되는 것과.

카톡에서도 메타데이터가 올바르게 적용된 사실을 확인할 수 있다.

 

 

 

 

 

 

 

 

참고 

패캠 강의 고석진 강사님의 LOVETRIP

728x90

리엑트에서는 보통 webpack,Roll up등을 이용하여서, 내 코드를 하나의 bundle 파일로 묶은 다음에  사용한다.

하나의 파일로 묶어서 관리하는 것 자체는 좋은데, 만약 App의 크기가 커지게 된 경우에는 초기 로딩속도가 느려지게 된다.

이를 해결하기 위한 방법인 code Splitting이다. 각각의 페이지에 필요한 내용을 쪼갠 후에 필요한 시점에 동적으로 로딩하는 방법이고, React에서는 Lazy를 통해서 이를 구현할 수 있다.

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

공식 문서에 있는 예시는 위와 같은데 React.lazy를 사용하여서 동적인 임포트를 하고, Suspense를 통해서 로딩되는 동안 보여줄 컴퍼넌트를 등록한다.

 

이를 앱에 등록하여 성능 탭을 통해서 확인할 수 있는데, 우리가 확인할 것은 FP(초기 페인팅)이다.

스플리팅 전에는 1.16초가 걸렸고 bundle.js 파일을 로드하는 데는 93ms이 걸렸는데

스플리팅 이후에는 0.77(774밀리)초가 걸렸고  bundle.js 로드 속도도 66ms로  로드 시간이 감소됐다.

FP의 시간이 34프로나 감소가 된다.

 

 

 

 

코드스플리팅 후

 

 

이후 컴퍼넌트 이동을 할 경우에 노란색 파일들이 먼저 로드되고, 이때 chunk파일들이 페이지마다 개별적으로 로드됨을 확인할 수 있다.

참고  문헌

https://legacy.reactjs.org/docs/code-splitting.html

 

Code-Splitting – React

A JavaScript library for building user interfaces

legacy.reactjs.org

 

728x90

LightHouse에서는 CLS (Cumlative Layout Shift)라고 Layout Shift 가 얼마나 일어나는지에 대한 점수를 매겨준다.

이때 Layout Shift란  컴퍼넌트나 이미지등이 화면에 그려질 때, 각각의 컴퍼넌트마다 그려지는 속도가 다르다.

(연산작업이 많거나 이미지를 그리는 경우 등등)

이때 위쪽에 있는 DOM요소가 높이를 차기하기 전에 아래쪽에 있는 먼저 로드가 되면 추후에 위쪽에 있는 컴 DOM요소가 가 로드가  완료되면서, 아래쪽에 있는 컴퍼넌트들이 아래로 밀려난다.

 

이 경우에 사용자는 버튼등을 클릭하려하는데, 갑자기 위치가 바뀌는 불편함을 겪게 되고, 이를 LightHouse에서는 CLS라는 점수로 측정해준다.

 

 

이를 해결하기 위한 대표적인 방법이 Skeleton UI로  관련 컴퍼넌트와 유사한 UI를 뼈대만 잡고, 해당 컴퍼넌트가 로드가 완료되면 교체하는 방식을 사용한다.

 

이를 리엑트의 Suspense를 사용하여 구현할 수 있는데, 로딩 중일 때는 Compound Component인 ListRow.Skeleton을 보여주고, 로딩이 완료가 되면 CardList를 랜더링하고 있습니다. 

 

<Suspense
        fallback={[...new Array(10)].map((_, idx) => (
          <ListRow.Skeleton key={idx} />
        ))}
      >
        <CardList />
      </Suspense>

이떄 현재 페이지는 무한스크롤을 통해 최초 10개만 보여주고 있기 때문에 fallback에서  Skeleton이미지를 10개 렌더링 하고 있습니다.

 

 

728x90

프로젝트를 실제로 배포할 때는, 우리는 작성한 코드를 빌드한 후에 웹서버를 통해서 배포를 하게 된다.

리액트는 기본적으로 SPA 방식이기 때문에, MPA와 달리 하나의 index.html파일을 사용합니다. 또한 이때 JS파일을 모두 로딩합니다. 그렇기 때문에 페이지가 많아 질수록, 프로젝트 규모가 커질수록 해당 파일의 크기가 커지고 초기 로딩속도가 느려지게 됩니다. 

 이때 초기 로딩 속도를 늘리기 위한 방법 중 하나로 Tree Shaking이라는 방법이 있습니다.

 

Tree Shaking이란 불필요한 코드를 제거하는  방법을 의미합니다.

 

https://github.com/rollup/rollup?tab=readme-ov-file#tree-shaking

 

1. 번들사이즈 분석하기

 

Tree Shaking이 효과적으로 적용되었는지 확인하기 위해서, https://www.npmjs.com/package/webpack-bundle-analyzer

 

webpack-bundle-analyzer

Webpack plugin and CLI utility that represents bundle content as convenient interactive zoomable treemap. Latest version: 4.10.1, last published: a month ago. Start using webpack-bundle-analyzer in your project by running `npm i webpack-bundle-analyzer`. T

www.npmjs.com

위 라이브러리를 통해서 각각의 JS파일이 얼만큼의 용량을 먹고 있고, Tree Shaking 이후 얼만큼 감소하는 지를 확인할 것 입니다.

 

해당 라이브러리를 개발자 의존성에 추가한 이후,  config 파일에 아래 코드를 추가해 줍니다.

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

 

저는 webpack 대신에 craco를 사용하여서 craco.config.js에 아래와 같이 코드를 추가했습니다.

const CracoAlias = require('craco-alias')
//  webpack-bundle-analyzer를 추가하고, 개발환경에서만 실행하도록 함
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

const isProduction = process.env.NODE_ENV === 'production'
module.exports = {
  plugins: [
    {
      plugin: CracoAlias,
      options: {
        source: 'tsconfig',
        tsConfigPath: 'tsconfig.paths.json',
      },
    },
  ],
  babel: {
    presets: [['@babel/preset-react', { runtime: 'automatic', importSource: '@emotion/react' }]],
    plugins: ['@emotion/babel-plugin'],
  },
  // 아래 부분
  webpack: {
    plugins: isProduction ? [] : [new BundleAnalyzerPlugin()],
  },
}

 

이후 프로젝트를 시작하면, 아래와 같은 창이 생깁니다.

각각의 javascript 파일의 크기를 확인할 수 있으며, 저는 lodash 파일을 트리쉐이킹해서 크기를 줄일 것 입니다.

 

 

우선 빌드된 파일의 크기를 확인해보면 아래와 같습니다.

적용전

이후 Tree Shaking을 통해서 감소시키면 됩니다.

적용 후

 

2. 트리쉐이킹 원리

위의 표를 통해서 간단하게 CJS 와 ESM의 차이를 알 수 있는데, 이때 주목해야할 점은 로딩방식의 차이점입니다.

CJS에서는 로딩이 동적이고 분석시점이 런타임이기에 빌드시에 필요성에 대한 유무를 명확히 알기 힘들지만, ESM은 정적입니다. 따라서 저희는 ESM 방식을 통하여서 필요한 코드들만 import 하여 사용할 수 있습니다.

 

저는 프로젝트에서 flatten 이외의 lodash의 기능을 사용하지 않았습니다. 따라서 lodash를 전체를 import 하는 것은 불필요한 코드를 많이 사용하게 됩니다.

https://www.npmjs.com/package/lodash

 

lodash

Lodash modular utilities.. Latest version: 4.17.21, last published: 3 years ago. Start using lodash in your project by running `npm i lodash`. There are 179589 other projects in the npm registry using lodash.

www.npmjs.com

따라서 기존 CJS의 lodash (위의 라이브러리) 대신에  lodash-es라는 라이브러리를 사용하는 것이 좋고, 더 나아가 flatten이라는 함수만 제공하는 lodash.flatten 라이브러리를 사용하여 트리쉐이킹을 진행할 것 입니다.

(lodash의 많은 유틸리티 함수를 사용한다면, lodash-es를 사용하고 , 저는 lodash flatten을 설치하였습니다.

https://www.npmjs.com/package/lodash.flatten

 

lodash.flatten

The lodash method `_.flatten` exported as a module.. Latest version: 4.4.0, last published: 7 years ago. Start using lodash.flatten in your project by running `npm i lodash.flatten`. There are 871 other projects in the npm registry using lodash.flatten.

www.npmjs.com

https://www.npmjs.com/package/lodash-es

 

lodash-es

Lodash exported as ES modules.. Latest version: 4.17.21, last published: 3 years ago. Start using lodash-es in your project by running `npm i lodash-es`. There are 9670 other projects in the npm registry using lodash-es.

www.npmjs.com

 

해당 라이브러리를 설치한 이후 기존의  import 문을 변경해주면 됩니다.

// import { flatten } from 'lodash'
import flatten from 'lodash.flatten'

 

 

자세한 CJS와 ESM의 차이를 알고 싶다면 카카오 기술블로그에서 내용을 읽어 보시는 것도 좋을 것 같아요.

 

https://tech.kakao.com/2023/10/19/commonjs-esm-migration/

 

CommonJS에서 ESM으로 전환하기

안녕하세요, FE플랫폼팀에서 FE 개발자를 위한 개발을 담당하는 Ethan입니다. 이 글에서는 운영 중인 서비스에서 사용하는 라이브러리의 버전을 업그레이드하는 과정에서, 기존에 사용하던 모듈

tech.kakao.com

 

728x90

패스트캠퍼스에서 최적화 강의를 듣는 도중에, useState에 초기값을 할당할 때 최적화 하는 방법에 대해서 배우게 되었다.

 

관련 코드는 아래와 같다.

const [termsAgreements, setTermsAgreements] = useState(() => {
    return 약관목록.reduce<Record<string, boolean>>(
      (prev, term) => ({
        ...prev,
        [term.id]: false,
      }),
      {},
    )

 

약관목록에서 키는 문자 타입의 term의 id값이고, value는 초기값 false인 것들을 termsAgreement의 초기값으로 할당할 것이다.

 

이때, 이미 만들어진 약관목록 상수값에서 해당 키값을 가져오지만, 이는 최초 렌더링시에 한 번만 실행되면 된다. 

const [termsAgreements, setTermsAgreements] = useState(
        약관목록.reduce<Record<string, boolean>>(
      (prev, term) => ({
        ...prev,
        [term.id]: false,
      }),
      {},
    )

 

하지만 아래와 같이 할당을 경우 useState가  초기값은 한번의 실행에서만 할당하지만, 매 랜더링마다 reduce함수가 실행되고, 이건 불필요한 성능 저하를 일으킨다.

 

따라서 이를 콜백함수 형태로 할당할 경우에 성능을 최적화 할 수 있다.

 

첫 번째 방식으로 할 경우에 최적화가 되는 이유는 리액트가 해당 함수의 결과값이 같은지를 확인하는게 아니라, 해당 함수가 같은지를 확인하기 때문이다.

 

https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state

 

useState – React

The library for web and native user interfaces

react.dev

 

728x90
const AuthInput: React.FC<AuthInputType> = ({ title, password, setValue }) => {
    const [isVisible, setIsVisible] = useState(password);
    const [inputValue, setInputValue] = useState('');

    const toggleIsPassword = () => {
        setIsVisible(!isVisible);
    };

    const onChangeInput = (e: ChangeEvent<HTMLInputElement>) => {
        setValue(e.target.value);
    };

 로그인 및  회원가입에 사용할 인풋창을 위와 같은 로직으로 구현하였다.
문제가 없어 보일 수 있지만, 성능적으론 굉장히 문제가 있는 코드이다.

리익트 리랜더링 조건으로는  

  1. state 변경이 있을 때
  2. 새로운 props이 들어올 때,
  3. 부모 컴퍼넌트가 렌더링 될 때
  4. props가 업데이트 될 때

4가지로 주로 꼽는다.

 

위 코드와 같이 부모 컴퍼넌트의 setter를 받는 컴퍼넌트는 부모 컴퍼넌트의 state를 변경 시킨다.
이는 부모 컴퍼넌트의 리렌더링을 유발하고 이에 의해서, 부모 컴퍼넌트 아래의 모든 컴퍼넌트를 리 랜더링 시킨다.

따라서 리랜더링을 줄이기 위해서 debounce를 사용하여서 제어하려고 한다.
debounce는 지정한 delay시간안에 추가적인 매개변수값의 변화(입력값)가 없는 경우에만 return 하게 하여 
한번에 여러 글자를 일어나는 경우 렌더링 횟수를 줄이기 위해서 사용한다.
https://dev.to/manishkc104/debounce-input-in-react-3726

 

Debounce Input in React

Debouncing an input is a technique used to improve web application performance and user experience....

dev.to

위 게시글을 보면 우선 자식 컴퍼넌트로 입력값을 받고 이 값의 변화를 useEffect로 인지하고, setTimeout과 clearTImeout을 활용하여  제어하려한다.

React.useEffect(() => {
  const delayInputTimeoutId = setTimeout(() => {
    setDebouncedInputValue(inputValue);
  }, 500);
  return () => clearTimeout(delayInputTimeoutId);
}, [inputValue, 500]);

이를 활용하여 내 코드도 리랜더링을 제어하려한다.

하지만 디바운스의 경우 범용성이 높기 떄문에 커스텀훅으로 만들 것 이다.


import { useEffect, useState } from 'react';

export const useDebounce = (value: string, delay: number) => {
	const [debounceValue, setDebounceValue] = useState(value);
	useEffect(() => {
		const handler = setTimeout(() => {
			setDebounceValue(value);
		}, delay);

		return () => {
			clearTimeout(handler);
		};
	}, [value, delay]);

	return debounceValue;
};

이후  input의 값과 딜레이를 훅에 인수로 전달할 것이다.
그리고 useEffect를 통해서 debounceValue가 변할 떄마다 부모 컴퍼넌트의 setter를 통해서 값을 할당하면 된다.

const AuthInput: React.FC<AuthInputType> = ({ title, password, setValue }) => {
	const [inputValue, setInputValue] = useState('');

	const onChangeInput = (e: ChangeEvent<HTMLInputElement>) => {
		setInputValue(e.target.value);
	};
	const debouncedValue = useDebounce(inputValue, 300);

	useEffect(() => {
		if (debouncedValue) {
			setValue(debouncedValue);
		}
	}, [debouncedValue, setValue]);

	return (
		<AuthInputWrapper>
			<AuthInputTitle>{title}</AuthInputTitle>
			<AuthInputContainer>
				<AuthInputSheet type={isVisible ? 'password' : 'text'} onChange={onChangeInput} />



사실 위 커스텀훅을 만들고, onChangeInput 내에서 훅을 사용하려다가  일반 함수내에서 훅을 사용하려다가 훅 규칙을 위반해서 에러도 겪었었지만, 결국 완성,,

+ Recent posts