๐Ÿ”ฎ ์†Œ๋งˆ๋ฒ• ํ”„๋กœ์ ํŠธ -12 (racingcar)

์ด๋ฒˆ์— ๋งŒ๋“ค์–ด๋ณธ ์†Œ๋งˆ๋ฒ• ํ”„๋กœ์ ํŠธ๋Š” ์ž๋™์ฐจ ๊ฒฝ์ฃผ๊ฒŒ์ž„ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ, OOP study ๋ฏธ์…˜์„ ๋ณ€ํ˜•ํ•ด์„œ ์ œ์ž‘ํ•ด๋ณด์•˜๋‹ค. MVVM ํŒจํ„ด๊ณผ FLUX ํŒจํ„ด ๊ตฌํ˜„๊ณผ ์œ ๋‹›ํ…Œ์ŠคํŠธ์— ์ค‘์‹ฌ์„ ๋‘์–ด์„œ ์ฝ”๋“œ๋ฅผ ์งœ๋ณด์•˜๋‹ค. ๋””์ž์ธ ํŒจํ„ด์— ๋Œ€ํ•œ ์„ค๋ช…๊ณผ ํ…Œ์ŠคํŠธ์ฝ”๋“œ ์ž‘์„ฑ ๋ฐฉ๋ฒ• ๋ฐ ํŒ์€ ์—ฌ๊ธฐ์— ํฌ์ŠคํŒ…ํ•˜์˜€๊ณ , ์—ฌ๊ธฐ์„œ๋Š” ๊ตฌํ˜„ํ•œ ์ฝ”๋“œ์— ๋Œ€ํ•ด์„œ ์ •๋ฆฌํ•ด๋ณด๊ณ ์ž ํ•œ๋‹ค.


#. Project Map


์ œ์ž‘๋…ธํŠธ ํ•œ๋ˆˆ์—๋ณด๊ธฐ[์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ]

1. Layout

1-1. ๋ฉ”์ธํ™”๋ฉด

racingcarmain

๋ฉ”์ธํ™”๋ฉด์—๋Š” ์ž๋™์ฐจ์ด๋ฆ„ ์ž…๋ ฅ input๊ณผ ์‹œ๋„ํšŸ์ˆ˜ ์ž…๋ ฅ input์„ ๋‘์—ˆ๋‹ค.

1-2. ๊ฒฐ๊ณผํ™”๋ฉด

racingcarmain2

๊ฒฐ๊ณผํ™”๋ฉด์—์„œ๋Š” 1์ดˆ๋งˆ๋‹ค ํšŸ์ˆ˜๋ฅผ ๋Š˜๋ฆฌ๋ฉฐ ๊ฐ ํšŸ์ˆ˜๋งˆ๋‹ค ์ž๋™์ฐจ์˜ ์ด๋™๊ฑฐ๋ฆฌ๋ฅผ ํ‘œ์‹œํ•˜๊ณ , ๋งˆ์ง€๋ง‰์—๋Š” ์ตœ์ข… ์šฐ์Šน์ž๋ฅผ ์ถœ๋ ฅํ•˜๋„๋ก ํ•˜์˜€๋‹ค.

2. Container

2-1. App.tsx

// App.tsx
const App = () => {
    return (
        <Container>
            <form onSubmit={onSubmit}>
                {/*...*/}
            </form>
            <Processes process={process} />
            <Result result={result} />
        </Container>
    )

์ „์ฒด ์ปจํ…์ธ  ๋ ˆ์ด์•„์›ƒ์„ ๊ตฌ์„ฑํ•˜๋Š” App.tsx ์—๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ์ž…๋ ฅ์„ ๋ฐ›๋Š” form ์—˜๋ฆฌ๋จผํŠธ, ๊ฒฐ๊ณผ๋ฅผ ์ถœ๋ ฅํ•˜๋Š” Processes ์ปดํฌ๋„ŒํŠธ, ๊ทธ๋ฆฌ๊ณ  ๊ฒฐ๊ณผ๋ฅผ ์ถœ๋ ฅํ•˜๋Š” Result ์ปดํฌ๋„ŒํŠธ ์ด 3๊ฐœ๋กœ ๊ตฌ์„ฑํ•˜์˜€๋‹ค.

2-2. input value

const [carNames, setCarNames] = useState('')
const [count, setCount] = useState('')

const onChangeCarNames = useCallback(e => {
  setCarNames(e.target.value)
}, [])

const onChangeCount = useCallback(e => {
  setCount(e.target.value)
}, [])

return (
  <input id="carNames" type="text" value={carNames} onChange={onChangeCarNames}></input>
  <input id="count" type="text" value={count} onChange={onChangeCount}></input>
)

์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ ์ƒํƒœ๊ด€๋ฆฌ๋Š” react hooks ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

3. Validator

์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋Š” formValidator ๋ชจ๋“ˆ์„ ๋งŒ๋“ค์–ด์„œ ๋”ฐ๋กœ ๊ด€๋ฆฌํ•˜์˜€๋‹ค.

// formValidator.ts

const validateInput = (carNames: string, count: string) => {
  let _carNames = carNames.split(',')
  _carNames = trimCarNameBlank(_carNames)
  _carNames = _carNames.filter(v => v !== '')

  if (_carNames.length === 0) {
    return 'CAR_NAME_IS_BLANK_ERROR'
  }

  if (count === '') {
    return 'COUNT_IS_BLANK_ERROR'
  }

  if (!checkCarNameLength(_carNames)) {
    return 'CAR_NAME_LENGTH_ERROR'
  }

  if (!checkCountIsNumber(count)) {
    return 'COUNT_IS_NOT_NUMBER_ERROR'
  }
  return _carNames
}

const checkCarNameLength = (carNames: string[]) => {
  const MAX_CARNAME_LENGTH = 5
  for (let i = 0; i < carNames.length; i++) {
    if (carNames[i].length > MAX_CARNAME_LENGTH) return false
  }
  return true
}

const trimCarNameBlank = (carNames: string[]) => {
  return carNames.map(carName => carName.trim())
}

const checkCountIsNumber = (count: string) => {
  if (count.match(/\D/g)) return false
  return true
}

export {
  validateInput,
  checkCarNameLength,
  trimCarNameBlank,
  checkCountIsNumber,
}
// App.tsx

import { validateInput } from '../modules/formValidator'

const validator = validateInput(carNames, count)
if (validator === 'CAR_NAME_IS_BLANK_ERROR') {
  return setCarNameIsBlankError(true)
}
if (validator === 'COUNT_IS_BLANK_ERROR') {
  return setCountIsBlankError(true)
}
if (validator === 'CAR_NAME_LENGTH_ERROR') {
  return setCarNameLengthError(true)
}
if (validator === 'COUNT_IS_NOT_NUMBER_ERROR') {
  return setCountIsNotNumberError(true)
}
const _carNames = validator

validateInput ํ•จ์ˆ˜๋กœ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ž๋™์ฐจ๋“ค ์ด๋ฆ„๊ณผ ์‹œ๋„ํšŸ์ˆ˜๋ฅผ ๋ฐ›์•„, ์ž๋™์ฐจ์ด๋ฆ„์ด ๊ณต๋ฐฑ์ด ์•„๋‹Œ์ง€, ์ž๋™์ฐจ์ด๋ฆ„์ด 5๋ณด๋‹ค ํฐ ๊ฒฝ์šฐ๊ฐ€ ์—†๋Š”์ง€, ์‹œ๋„ํšŸ์ˆ˜๊ฐ€ ๊ณต๋ฐฑ์ด ์•„๋‹Œ์ง€, ์‹œ๋„ํšŸ์ˆ˜๊ฐ€ ์ˆซ์ž์ธ์ง€ ๊ฒ€์‚ฌํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค.

๋งŒ์•ฝ์— ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ์‹œ, react hooks๋ฅผ ์ด์šฉํ•ด์„œ ํ•ด๋‹น ์กฐ๊ฑด์— ๋งž๊ฒŒ Error ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋„๋ก ์งœ์ฃผ์—ˆ๋‹ค.

// App.tsx

return (
  <div>
    {carNameLengthError && (
      <div style={{ color: 'red' }}>์—๋Ÿฌ! ์ž๋™์ฐจ์ด๋ฆ„์€ 5์ดํ•˜๋กœ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค</div>
    )}
    {carNameIsBlankError && (
      <div style={{ color: 'red' }}>
        ์—๋Ÿฌ! ์ž๋™์ฐจ์ด๋ฆ„์€ ๊ณต๋ฐฑ์ด ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค
      </div>
    )}
    {countIsBlankError && (
      <div style={{ color: 'blue' }}>
        ์—๋Ÿฌ! ์‹œ๋„ํ•  ํšŸ์ˆ˜๋Š” ๊ณต๋ฐฑ์ด ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค
      </div>
    )}
    {countIsNotNumberError && (
      <div style={{ color: 'blue' }}>์—๋Ÿฌ! ์‹œ๋„ํ•  ํšŸ์ˆ˜๋Š” ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•˜์„ธ์˜ค</div>
    )}
  </div>
)

Error ์ƒํƒœ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค์‹œ ์ƒˆ๋กœ์šด ์ •๋ณด๋ฅผ ์ž…๋ ฅํ–ˆ์„ ๋•Œ๋Š” reset ๋˜๋„๋ก ์ฝ”๋“œ๋ฅผ ์งœ์ฃผ์—ˆ๋‹ค.

// App.tsx

const onSubmit = useCallback(
  e => {
    e.preventDefault()

    setCarNameIsBlankError(false)
    setCarNameLengthError(false)
    setCountIsBlankError(false)
    setCountIsNotNumberError(false)
  },
  [carNames, count]
)

4. Car ๊ฐ์ฒด

4-1. Car class

Car ๊ฐ์ฒด๋Š” classํ˜•ํƒœ๋กœ ๋งŒ๋“ค์–ด์ฃผ์—ˆ๊ณ , name,position ํ”„๋กœํผํ‹ฐ์™€ go ๋ฉ”์†Œ๋“œ๋ฅผ ๋„ฃ์—ˆ๋‹ค.

// Car.ts

export default class Car {
  name: string
  position = 0

  constructor(name: string) {
    this.name = name
  }
  go() {
    this.position = this.position + 1
  }
}

4-1. Car ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ

Car๊ฐ์ฒด๋Š” ์ƒ์„ฑ์žํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•ด์„œ Car ๊ฐ์ฒด ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค๊ณ , ์ž…๋ ฅํ•œ ์ž๋™์ฐจ์˜ ์ˆซ์ž๋งŒํผ ๋ฐฐ์—ด์— pushํ•˜๋„๋ก ์ฝ”๋“œ๋ฅผ ์งฐ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ชจ๋“  ์ž๋™์ฐจ์— ๋Œ€ํ•œ ๋กœ์ง์€ cars ๋ฐฐ์—ด์—์„œ ๋‹ด๋‹นํ•  ์ˆ˜ ์žˆ๊ฒŒ๋” ํ•ด์ฃผ์—ˆ๋‹ค.

const makeCars = (carNames: string[]) => {
  const _cars = []
  for (let i = 0; i < carNames.length; i++) {
    _cars.push(new Car(carNames[i]))
  }
  return _cars
}

const cars = makeCars(carNames)

5. racingCar ๋ชจ๋“ˆ

5-1. ์ž๋™์ฐจ ์ด๋™๊ณผ ๊ฒฐ๊ณผ ์ถœ๋ ฅ

์ž๋™์ฐจ ์ด๋™์— ๋Œ€ํ•œ ์ถœ๋ ฅ๊ณผ ๊ฒฐ๊ณผ ์ถœ๋ ฅ์€ setTimeout API์™€ hooks๋ฅผ ์ด์šฉํ•ด์„œ, 1์ดˆ๋งˆ๋‹ค ์ž๋™์ฐจ๋ฅผ ์ด๋™ํ•˜๊ณ  ์ถœ๋ ฅ์„ ๊ฐฑ์‹ ํ•˜๋„๋ก ์ฝ”๋“œ๋ฅผ ์งœ์ฃผ์—ˆ๋‹ค.

const [process, setProcess] = useState<null | JSX.Element>(null)
const [result, setResult] = useState<null | JSX.Element>(null)

let _count = Number(count)
for (let i = 0; i < _count; i++) {
  timer = setTimeout(() => {
    moveCars(cars)
    setProcess(makeProcess(i, cars))
    if (i === _count - 1) {
      setResult(makeResult(cars))
    }
  }, 1000 * i)
}

5-2. racingCar ํ•จ์ˆ˜ ํ๋ฆ„

// racingCarModule
{
moveCars : '์ž๋™์ฐจ๋“ค ์ด๋™',
moveCar  : '์ž๋™์ฐจ ๊ฐ์ฒด ํ•˜๋‚˜ ์ด๋™',
checkMoveCarCondition : '์ž๋™์ฐจ๊ฐ€ ์ด๋™ํ• ์ง€๋ง์ง€ ์กฐ๊ฑด์ฒ˜๋ฆฌ',
makeProcess : '์ž๋™์ฐจ ์ด๋™๊ฑฐ๋ฆฌ์— ๋Œ€ํ•œ ์ถœ๋ ฅ(jsx) ๋ฆฌํ„ด',
makeDistance : '์ž๋™์ฐจ ์ด๋™๊ฑฐ๋ฆฌ(-) ๋ฌธ์ž์—ด ๋ฆฌํ„ด',
makeResult : '์ตœ์ข… ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ์ถœ๋ ฅ(jsx) ๋ฆฌํ„ด',
getWinner : 'position๊ฐ’์ด ๊ฐ€์žฅ ํฐ ์ž๋™์ฐจ ๋ฆฌํ„ด',
}

์ด๋Ÿฐ ์‹์œผ๋กœ, ํ•จ์ˆ˜๋Š” ์ž๊ธฐ ๊ธฐ๋Šฅ๋งŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ตœ๋Œ€ํ•œ ๋งŽ์ด ์ชผ๊ฐœ์ฃผ์—ˆ๋‹ค.

6. Test

import {
  checkCarNameLength,
  trimCarNameBlank,
  checkCountIsNumber,
  validateInput,
} from './formValidator'

describe('validateInput ํ•จ์ˆ˜', () => {
  it('๋นˆ๋ฌธ์ž์—ด์ผ ๋•Œ, ์ž˜ ๊ฑธ๋Ÿฌ๋‚ด๋Š”์ง€ ํ™•์ธ', () => {
    expect(validateInput('', '')).toBe('CAR_NAME_IS_BLANK_ERROR')
  })
  it('์ž๋™์ฐจ์ด๋ฆ„๊ธธ์ด๊ฐ€ 5์ดˆ๊ณผํ•  ๊ฒฝ์šฐ ์ž˜ ๊ฑธ๋Ÿฌ๋‚ด๋Š”์ง€ ํ™•์ธ', () => {
    expect(validateInput('123456', '')).toBe('CAR_NAME_LENGTH_ERROR')
  })
  it('์‹œ๋„ํ•  ํšŸ์ˆ˜๊ฐ€ ์ˆซ์ž๊ฐ€ ์•„๋‹ ๊ฒฝ์šฐ ์ž˜ ์ œ๊ฑฐ๋˜๋Š”์ง€ ํ™•์ธ', () => {
    expect(validateInput('126,a', 'aa')).toBe('COUNT_IS_NOT_NUMBER_ERROR')
  })
})

describe('checkCarNameLength ํ•จ์ˆ˜', () => {
  it('์ž๋™์ฐจ์ด๋ฆ„๊ธธ์ด๊ฐ€ 5์ดˆ๊ณผํ•  ๊ฒฝ์šฐ ์ž˜ ๊ฑธ๋Ÿฌ๋‚ด๋Š”์ง€ ํ™•์ธ', () => {
    expect(checkCarNameLength(['123456', '123'])).toBe(false)
  })
})

describe('trimCarNameBlank ํ•จ์ˆ˜', () => {
  it('์ž๋™์ฐจ์ด๋ฆ„์ด ๊ณต๋ฐฑ์ผ ๊ฒฝ์šฐ ์ž˜ ์ œ๊ฑฐ๋˜๋Š”์ง€ ํ™•์ธ', () => {
    expect(trimCarNameBlank(['   ', ' '])).toStrictEqual(['', ''])
  })
})

describe('checkCountIsNumber ํ•จ์ˆ˜', () => {
  it('์‹œ๋„ํ•  ํšŸ์ˆ˜๊ฐ€ ์ˆซ์ž๊ฐ€ ์•„๋‹ ๊ฒฝ์šฐ ์ž˜ ์ œ๊ฑฐ๋˜๋Š”์ง€ ํ™•์ธ', () => {
    expect(checkCountIsNumber('ใ…')).toStrictEqual(false)
  })
  it('์‹œ๋„ํ•  ํšŸ์ˆ˜๊ฐ€ ์ˆซ์ž๊ฐ€ ์•„๋‹ ๊ฒฝ์šฐ ์ž˜ ์ œ๊ฑฐ๋˜๋Š”์ง€ ํ™•์ธ', () => {
    expect(checkCountIsNumber('123a')).toStrictEqual(false)
  })
  it('์‹œ๋„ํ•  ํšŸ์ˆ˜๊ฐ€ ์ˆซ์ž๊ฐ€ ์•„๋‹ ๊ฒฝ์šฐ ์ž˜ ์ œ๊ฑฐ๋˜๋Š”์ง€ ํ™•์ธ', () => {
    expect(checkCountIsNumber('123')).toStrictEqual(true)
  })
})

ํ…Œ์ŠคํŠธ๋Š” ์•„์ง ์ต์ˆ™์ง€ ์•Š๋Š” ๊ฒƒ ๊ฐ™๋‹ค. ์ข€ ๋” ํ•™์Šตํ•  ๊ฒƒ! ํ…Œ์ŠคํŠธ ์ž‘์„ฑ๊ทœ์น™ ๋ฐ ํŒ

7. Self-Feedback

7-1. clearTimeout

์‚ฌ์šฉ์ž๊ฐ€ submit ์„ ์—ฐ์†์ ์œผ๋กœ ์ˆ˜ํ–‰ํ–ˆ์„ ๋•Œ, ๊ธฐ์กด์˜ timer๋“ค๋„ ๊ฐ™์ด ์ค‘์ฒฉ๋˜์–ด ์‹คํ–‰๋˜๋ฉฐ, ๋ทฐ๋ฅผ ๊ฒน์ณ์„œ ์—…๋ฐ์ดํŠธํ•˜๋Š” ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋‹ค. ์ž…๋ ฅ ์ด๋ฒคํŠธ๋ฅผ ์ œํ•œํ•˜๊ฑฐ๋‚˜ timer๋ฅผ ์žฌ์„ค์ •ํ•ด์ฃผ๋Š” ๋“ฑ์˜ ์ž‘์—…์ด ํ•„์š”ํ•  ๊ฒƒ ๊ฐ™๋‹ค.


Written by@taenyKim
๋ฐฐ์šฐ๋ฉฐ ์„ฑ์žฅํ•˜๊ณ  ๊ธฐ๋กํ•˜๊ธฐ #FE #UI #๊ฐœ๋ฐœ #life

GitHubFacebook