Poke Docs 만들기 - Snowflake 효과, object case converter

엄현태·2021년 2월 9일
1

poke-docs

목록 보기
3/4
post-thumbnail

Snowflake 효과

snowflake 효과란 카카오톡에서 쉽게 만날 수 있는데요. 눈이 오는 날에 채팅방에 눈이 오는 효과 입니다.
저의 포켓몬 도감에서는 메인페이지에서 포켓몬이 눈처럼 내려오는 효과가 되어있는데요.

생각보다 구현이 어렵지 않습니다.

components/Thumbnailes

import { PokemonThumbnail, Thumbnail } from 'components/Thumbnail';
import { IMAGE_URL, TITLE_IMAGE } from 'constants/common';
import { useGetPokemonList } from 'hooks/useGetPokemonList';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import * as Styles from './styles';

export const Thumbnailes = () => {
  const router = useRouter();
  const { data, isLoading } = useGetPokemonList();
  const pokemonList = useMemo(() => {
    if (data) {
      return data.results.map((item, index) => ({
        ...item,
        number: index + 1,
      }));
    }
    return [];
  }, [data]);
  const [thumbnailes, setThumbnailes] = useState<PokemonThumbnail[]>([]);

  useEffect(() => {
    setThumbnailes(
      pokemonList.map(pokemon => ({
        ...pokemon,
        image: `${IMAGE_URL}${pokemon.number}.png`,
      })),
    );
  }, [pokemonList]);

  const handleClickFindPokemon = useCallback(() => {
    router.push('docs');
  }, [router]);

  const pokemonImages = useMemo(
    () => (
      <>
        {thumbnailes.map((thumbnail, index) => (
          <Styles.Thumbnail
            delay={Math.random() * 5}
            left={Math.random() * 100}
            // eslint-disable-next-line react/no-array-index-key
            key={index}
          >
            <Thumbnail pokemon={thumbnail} />
          </Styles.Thumbnail>
        ))}
      </>
    ),
    [thumbnailes],
  );

  if (isLoading || !data) {
    return <div>Loading...</div>;
  }

  return (
    <Styles.Container>
      <Styles.Image src={TITLE_IMAGE} alt="titleImage" />
      <Styles.Text onClick={handleClickFindPokemon}>
        {'포켓몬 찾기 >'}
      </Styles.Text>
      {pokemonImages}
    </Styles.Container>
  );
};

위에 코드에서 보시면 Styles.Thumbnail 이 핵십입니다. 각각의 element(포켓몬)들에 스타일을 주어서 위에서부터 내려오게 하는 효과입니다.
각각의 element를 absolute 로 감싼 뒤에 animation효과를 주면 됩니다.
말로 설명하기 보다는 코드를 먼저 보여드리겠습니다.

Thumbnailes/styles.ts

interface ThumbnailProps {
  delay: number;
  left: number;
}

export const Thumbnail = styled.div<ThumbnailProps>`
  position: absolute;
  animation-name: rainDown;
  animation-duration: 5.5s;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in;
  animation-delay: ${props => props.delay}s;
  left: ${props => props.left}%;
  z-index: 10;

  @keyframes rainDown {
    90% {
      opacity: 1;
    }
    70% {
      opacity: 0.7;
      transform: translate(0, 97vh);
    }
    100% {
      opacity: 0;
      transform: translate(0, 100vh);
    }
  }
`;

이런식으로 rainDown 애니메이션을 정의한 뒤 animation- 효과들을 정의해주면 됩니다. 각각의 요소마다 내리는 위치와 시간이 달라야하기 때문에 각각의 요소는 delay, left 를 props 를 받아서 전달해주게끔 하였습니다. 두 값은 랜덤값이 되고 눈이 내리는것 처럼 효과가 나오게 되죠!

한번 애니메이션 값을 바꾸면서 진행해보시면 될듯 합니다!

object case converter

이건 프로젝트와 직접적인 연관이 없는 이야기인데 보통 서버에는 snake_case 를 사용하고 클라이언트에서는 camelCase 를 사용하게 됩니다. 따라서 서버에서 넘어오는 object key를 converter해주는 작업이 필요한데요. 이를 간단하게 regex 를 이용해서 해줄 수 있도록 만들어 보았습니다.
이 포켓몬 프로젝트를 진행하면서 만든 모듈 이기 때문에 포스팅 해봤습니다.

object-case-converter
레포를 보시면 아주 간다한데요. index.js 파일 하나 typescript 를 이용하기 위한 index.d.ts 타입정의파일 하나 이렇게 두가지 입니다.

const decamelize = (object) => {
  if (!object) {
    return object;
  }

  const keys = Object.keys(object);
  const newKeys = keys.map((key) => {
    return key
      .replace(/([a-z\d])([A-Z])/g, (match, p1, p2) => {
        if (match) {
          return `${p1}_${p2}`;
        }
      })
      .replace(/([A-Z]+)([A-Z][a-z\d]+)/g, (match, p1, p2) => {
        if (match) {
          return `${p1}_${p2}`;
        }
      })
      .toLowerCase();
  });

  return newKeys.reduce((acc, cur, index) => {
    const obj = object[keys[index]];

    let value = obj;
    if (Array.isArray(obj)) {
      value = obj.map((item) => {
        return typeof item === 'object' ? decamelize(item): item;
      });
    } else if (typeof obj === 'object') {
      value = decamelize(obj);
    }

    return {
      ...acc,
      [cur]: value,
    }
  }, {})
}

const camelize = (object) => {
  if (!object) {
    return object;
  }

  const keys = Object.keys(object);
  const newKeys = keys.map((key) => {
    return key.replace(/[_]+(\w)/g, (match, p1) => {
      if (match) return p1.toUpperCase();      
    });
  });

  return newKeys.reduce((acc, cur, index) => {
    const obj = object[keys[index]];

    let value = obj;
    if (Array.isArray(obj)) {
      value = obj.map((item) => {
        return typeof item === 'object' ? camelize(item): item;
      });
    } else if (typeof obj === 'object') {
      value = camelize(obj);
    }

    return {
      ...acc,
      [cur]: value,
    }
  }, {})
}

module.exports.decamelize = decamelize;
module.exports.camelize = camelize;

실제로 구현부 또한 굉장히 간단한데요. 재귀를 활용하여 모든 key 값을 바꾸어주도록 하였습니다.

  • decamelize 할 경우 소문자대문자 인경우를 찾아서 소문자_소문자 로 바꾸어주도록 하였고, 대문자가 반복하여 나오는 경우에도 처리가 가능합니다.
    쉽게 말하여 AAbb -> a_abb 를 대응하기 위함입니다.
  • camelize 인 경우 훨씬 간단한데요. regex 를 통해서 a_b 패턴을 찾은 뒤에 aB 로 만들어 줍니다.

한가지 이슈가 있었던게 array 의 type 을 objet 로 인식하는 javascript 특성 때문에 먼저 array 이인지 판단하는 로직이 들어가게 되었습니다.
이슈나 PR 있으면 적극 환영합니다 ㅎㅎ

다음에는 포켓몬 상세 페이지 및 페이지 이동 관련하여 next 포스팅을 해보겠습니다.

profile
개발을 취미로 하는 개발자가 되고픔

0개의 댓글