# Today I Learn - 2022.05.10
# Pre Onboarding Course
- 그립 기획서 작성 : 피그마로 개인프로젝트 그립 무비 앱 기획서를 작성하였다. but 아직 미완성...
- 그립 무비 앱 코드 리뷰 : 로켓런치 장수영님이 무비 앱 코드 리뷰를 진행해주셨다.
- 타입스크립트 연습 : 타입스크립트 적용하는 것 연습해보기
// 1
// onClick?: (e: MouseEvent<HTMLButtonElement>) => void
// setChecked?: (e: ChangeEvent<HTMLInputElement>) => void
// onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void
// postSubmit: (e: FormEvent<HTMLFormElement>) => void
// 2
interface ButtonProps extends Omit<ButtonHTMLAttributes<unknown>, 'translate'> {
children: string | JSX.Element | JSX.Element[]
className?: string
disabled?: boolean
isSubmit?: boolean
loading?: boolean
name?: string
onClick?: (e: MouseEvent<HTMLButtonElement>) => void
btnRef?: RefObject<HTMLButtonElement>
size: 'unset' | 'big' | 'medium' | 'small' | 'smallest'
shape: 'unset' | 'reward' | 'rounded' | 'primary' | 'order' | 'ghost' | 'ghostLight' | 'line'
translate?: boolean
url?: string
inline?: boolean
danger?: boolean // ghost 빨간 버전
}
interface ButtonProps {
text: string
type:
| 'textWithArrow'
| 'textWithCopied'
| 'textWithIcon'
| 'copyButtonOnly'
| 'iconOnly'
| 'textOnly'
| 'textToCopied'
handleCopy?: () => void
}
interface Props {
className?: string
children: ReactNode
asFooter?: boolean
}
// 3
interface Props {
transactionUrl: string
currency: string
txId: string
transferState: ETransferState
arrow?: boolean
inline?: boolean
shape: 'line' | 'rounded'
}
export const ButtonTXID = ({
transactionUrl,
currency,
txId,
transferState,
arrow,
inline,
shape,
}: Props): JSX.Element | null => {
if (!currency || !txId) return null
if (!isTxidAvailable(transferState)) return null
return (
<Button url={transactionUrl} className={styles.txidButton} shape={shape} size='unset' inline={inline}>
<span>TX ID</span>
{arrow ? <ArrowIcon /> : <></>}
</Button>
)
}
// 4
import styles, { cx } from 'styles'
import { useI18n } from 'hooks'
import Lottie from 'utils/lottie'
import LoadingAni from '@/assets/animations/loading.json'
import LoadingAniDark from '@/assets/animations/loading-dark.json'
interface Props {
size: 'tiny' | 'small' | 'big' | 'bigger' | 'large' | 'huge'
withLabel?: boolean
className?: string
forceDark?: boolean
forceBright?: boolean
full?: boolean
}
export const LoadingSpinner = (props: Props): JSX.Element => {
const { size, withLabel, className, forceDark, forceBright, full } = props
const t = useI18n()
const wrapperClassName = cx(styles.loadingSpinner, className, styles[size], {
[styles.forceDark]: forceDark,
[styles.forceBright]: forceBright,
[styles.full]: full,
})
return (
<div className={wrapperClassName}>
<Lottie animationData={LoadingAni} className={styles.brightSpinner} />
<Lottie animationData={LoadingAniDark} className={styles.darkSpinner} />
{withLabel && <p>{t('front:global.loading')}</p>}
</div>
)
}
// 5
function getActiveWidth(
percentage: number,
isDesktop: boolean,
mobileMaxWidth: number,
atTrade?: boolean,
atWallet?: boolean): number {
if (atTrade) {
const maxWidth = isDesktop ? 295 : mobileMaxWidth
const dirtyWidth = maxWidth * percentage
const cleanWidth = dirtyWidth - (dirtyWidth % 5) // 5의 배수로 고정
return Math.max(cleanWidth, 5)
}
if (atWallet) {
const maxWidth = isDesktop ? 480 : mobileMaxWidth
const dirtyWidth = maxWidth * percentage
const cleanWidth = dirtyWidth - (dirtyWidth % 8) // 8의 배수로 고정
return Math.max(cleanWidth, 8)
}
return 0
}
// 6
import { Dispatch, MouseEvent, SetStateAction } from 'react'
import styles, { cx } from 'styles'
import { useI18n } from 'hooks'
import { LoadingSpinner } from '@/components/_common/LoadingSpinner'
export interface SwitchItem {
name: string
label?: string
icon?: JSX.Element
disabled?: boolean
}
interface Props<T> {
items: SwitchItem[]
mode: T
setMode: (newMode: T) => void | Dispatch<SetStateAction<T>>
className?: string
type: 'dTag' | 'default'
color: 'common' | 'trade' | 'setting'
isLoading?: boolean
}
export const CommonSwitch = <T extends string>({
items,
mode,
setMode,
className,
type,
color,
isLoading,
}: Props<T>): JSX.Element => {
const t = useI18n()
const handleMode = (e: MouseEvent<HTMLButtonElement>): void => {
const { name } = e.currentTarget
setMode(name as T)
}
const wrapperClassName = cx(styles.commonSwitch, styles[type], styles[color], className, {
[styles.right]: mode === items[1].name,
})
if (isLoading) {
return (
<div className={wrapperClassName}>
<LoadingSpinner size='small' forceDark />
</div>
)
}
return (
<div className={wrapperClassName}>
{items.map((item) => {
return (
<button
key={commonSwitch-${item.name}}
type='button'
onClick={handleMode}
name={item.name}
disabled={item.disabled}
className={cx({ [styles.active]: mode === item.name })}
tabIndex={-1}
>
{item.icon || t(item.label || '')}
</button>
)
})}
<div className={styles.aniBg} />
</div>
)
}
// 7
import { MouseEvent } from 'react'
import DatePicker from 'react-datepicker'
import styles from 'styles'
import { SetterOrUpdater } from 'stateHooks'
import { useClickAway, useGA, useI18n, useRef, useState } from 'hooks'
import { TDateFilter } from 'types/histories.d'
import { Button } from '../_common/Button'
import DatePickerHeader from './DatePickerHeader'
import { CommonButtons } from '@/components/_common/Buttons'
interface Props {
date: TDateFilter
setDate: SetterOrUpdater<TDateFilter> | ((value: Date | null) => void)
handleClose: () => void
gaAction: string
}
const CustomDatePicker = ({ date, setDate, handleClose: handleClose2, gaAction }: Props): JSX.Element => {
const datePickerRef = useRef<HTMLDivElement>(null)
const { gaEvent } = useGA()
const t = useI18n()
const [selected, setSelected] = useState(date)
useClickAway(datePickerRef, handleClose2)
const renderDayContents = (day: number): JSX.Element => {
return (
<div>
<span className={styles.dayContent}>{day}</span>
</div>
)
}
const handleChange = (d: Date): void => setSelected(d)
const handleConfirm = (): void => {
setDate(selected)
handleClose2()
gaEvent({ action: `${gaAction}_calendar_search` })
}
const handleClose = (e: MouseEvent<HTMLButtonElement>) => {
return e
}
// 8
return (
<div className={styles.datePicker} ref={datePickerRef} translate='no'>
<DatePicker
selected={selected}
onChange={handleChange}
inline
renderDayContents={renderDayContents}
maxDate={new Date()}
showMonthDropdown
useShortMonthInDropdown
renderCustomHeader={({ date: d, changeYear, changeMonth, decreaseMonth, increaseMonth }): JSX.Element => (
<DatePickerHeader
date={d}
changeYear={changeYear}
changeMonth={changeMonth}
decreaseMonth={decreaseMonth}
increaseMonth={increaseMonth}
/>
)}
/>
<CommonButtons asFooter>
<Button shape='ghost' size='big' onClick={handleClose} translate>
{t('front:global.cancel')}
</Button>
<Button shape='primary' size='big' onClick={handleConfirm} translate>
{t('front:global.go')}
</Button>
</CommonButtons>
</div>
)
}
export default CustomDatePicker
// 9
import { ChangeEvent, Dispatch, MouseEvent, SetStateAction, useMemo } from 'react'
import styles, { cx } from 'styles'
import { useState } from 'hooks'
import { PageHandleIcon, TooltipIcon } from 'svgs'
interface SliderProps {
currentPage: number
totalPage: number
tooltips?: string[]
setPage: Dispatch<SetStateAction<number>> | ((value: number) => void)
wrapperWidth: number
scrollToTop: () => void
}
const Slider = ({
currentPage,
totalPage,
tooltips,
setPage,
wrapperWidth,
scrollToTop,
}: SliderProps): JSX.Element | null => {
const [hover, setHover] = useState<{
date?: string
x?: number
}>({})
const sliderX = useMemo(() => currentPage - 1, [currentPage])
const handleOnChange = (event: ChangeEvent<HTMLInputElement>): void => {
const { value } = event.currentTarget
setPage(Number(value) + 1)
}
const handleMouseOut = (): void => setHover({})
const handleHover = (e: MouseEvent): void => {
if (!tooltips) return
const { x } = e.currentTarget.getBoundingClientRect()
const hoverWidth = wrapperWidth / totalPage
const hoveredPageNumber = parseInt(`${(e.clientX - x) / hoverWidth}`, 10)
const date = tooltips[hoveredPageNumber]
if (!date) return
setHover({
date,
x: e.clientX - x,
})
}
// 10
return (
<div className={styles.sliderWrapper} onMouseMove={handleHover} onMouseOut={handleMouseOut} onBlur={handleMouseOut}>
<input
type='range'
step={0}
min={0}
max={totalPage - 1}
value={sliderX}
onChange={handleOnChange}
onMouseUp={scrollToTop}
/>
<div
className={cx(styles.fakeHandle, { [styles.hover]: hover.date })}
style={{
left: `${(wrapperWidth / totalPage) * sliderX}px`,
width: `${wrapperWidth / totalPage}px``,
}}
>
<PageHandleIcon />
</div>
{hover.date && (
<div className={styles.hoverDate} style={{ left: hover.x }}>
<p>{hover.date}</p>
<TooltipIcon role='tooltip' />
</div>
)}
<div className={styles.fakeTrack} />
</div>
)
}
export default Slider
// 11
export interface WalletDataCore {
flatFee: string
rateFee: string
minAmount: string
maxAmount: string
validFrom: number
}
export interface FeeListItem extends WalletDataCore {
blockchainName: string
currency: string
}
export interface FeeListData {
withdrawalFees: FeeListItem[]
}
export interface WithdrawBalanceData extends WalletDataCore {
withdrawableAmount24h: string
}
export interface AddressData {
address: string
addressParams?: {
destinationTag: string
}
}
export interface WithdrawalParams {
blockchainName?: string
currency: string
address: string
addressParams?: {
destinationTag: string
}
netAmount: string
fee: string
token: string
}
export interface WithdrawalSucessData {
id: string
}
export interface WithdrawalErrorData {
error: string
errorDetails: {
reason: string
expiredAt: number
}
}
// 12
/* eslint-disable camelcase */
export interface ZendeskArticle {
id: number
html_url: string
created_at: string
section_id: number
title: string
body: string
}
export interface ZendeskArticles {
articles: ZendeskArticle[]
count: number
next_page: boolean
page: number
page_count: number
per_page: number
previous_page: boolean
sort_by: string
sort_order: string
}
- eslint & prettier의 중요성 : eslint & prettier 항상 적용해야한다. error나 warning이 절대 보이지 않게 해달라고 다시 한번 강요하셨다.
- README 작성 : README는 확인하는 사람이 폴더 구조를 생각할 수 있게 해주도록 프로젝트의 설계를 파악할 수 있게끔 작성한다.
- scss module 파일명 : example.module.scss와 같이 scss의 경우 파일명은 소문자로 작성하는 것이 좋다.
- 공통 컴포넌트 : components 폴더 내부에 공통으로 쓰는 경우 컴포넌트는 common 폴더를 하나 만들어서 그 내부에 사용할 수도 있다. 이렇게 공통으로 쓰는 컴포넌트를 별도로 관리하여 다른 프로젝트 진행 시 common 폴더 자체를 복붙해서 템플릿처럼 사용할 수 있다.
- children과 outlet : 최근 react에서는 children을 대체하여 outlet을 사용하기도 한다.
- suspense : use suspense를 사용하여 spinner 구현 코드를 깔끔하게 작성할 수 있다. (참고 : https://ko.reactjs.org/docs/concurrent-mode-suspense.html)
- api의 instance화 : api는 src -> lib -> services 폴더 내부에 인스턴스로 만들어 사용한다.
- axios vs ky : axios는 ky와 다르게 time out을 잘 잡지 못한다.
- 디바운스와 스로틀링 : 디바운스(Debounce)와 스로틀(Throttle)를 활용한다. 수영님이 추천해주신 디바운스 시간은 300을 추천하셨고 원래는 서버개발자랑 맞추어가는 것이 좋다고 한다. (참고 : https://www.zerocho.com/category/JavaScript/post/59a8e9cb15ac0000182794fa)
- import, state 정리 : import할 때나 state부분에 용도 혹은 어디서 가져오냐를 구분하여 정리해두면 가독성이 좋아진다. import의 경우 예시로 라이브러리 -> 훅 or 타입 -> svg의 순서로 나누어 정리한다.
- 중첩 구조분해할당 : 구조분해할당 사용 시 두번의 구조분해가 필요한 경우 한번에 해도 가독성이 괜찮다면 중첩하여 한번에 구조분해하여 코드의 길이를 줄이고 가독성을 좋게한다.
- ts에서 선언식과 표현식 : 타입스크립트에서 함수 선언식이나 함수 표현식이나 타입추론이 달랐던 차이가 있었으나 버전이 업그레이드되면서 무관해졌다. (ex const Component (() => {}) / function Component() {})
- state의 배열 : state의 값이 동적으로 들어가며 그 값이 array인 경우 초기값을 넣지 않는 것보다 빈 array로 넣어주는 것이 좋다.