April 28, 2020
์ด๋ฒ์ ๋ง๋ค์ด๋ณธ ์๋ง๋ฒ ํ๋ก์ ํธ๋ ์๋์ฐจ ๊ฒฝ์ฃผ๊ฒ์ ์ดํ๋ฆฌ์ผ์ด์ ์ผ๋ก, OOP study ๋ฏธ์ ์ ๋ณํํด์ ์ ์ํด๋ณด์๋ค. MVVM ํจํด๊ณผ FLUX ํจํด ๊ตฌํ๊ณผ ์ ๋ํ ์คํธ์ ์ค์ฌ์ ๋์ด์ ์ฝ๋๋ฅผ ์ง๋ณด์๋ค. ๋์์ธ ํจํด์ ๋ํ ์ค๋ช ๊ณผ ํ ์คํธ์ฝ๋ ์์ฑ ๋ฐฉ๋ฒ ๋ฐ ํ์ ์ฌ๊ธฐ์ ํฌ์คํ ํ์๊ณ , ์ฌ๊ธฐ์๋ ๊ตฌํํ ์ฝ๋์ ๋ํด์ ์ ๋ฆฌํด๋ณด๊ณ ์ ํ๋ค.
๋ฉ์ธํ๋ฉด์๋ ์๋์ฐจ์ด๋ฆ ์ ๋ ฅ input๊ณผ ์๋ํ์ ์ ๋ ฅ input์ ๋์๋ค.
๊ฒฐ๊ณผํ๋ฉด์์๋ 1์ด๋ง๋ค ํ์๋ฅผ ๋๋ฆฌ๋ฉฐ ๊ฐ ํ์๋ง๋ค ์๋์ฐจ์ ์ด๋๊ฑฐ๋ฆฌ๋ฅผ ํ์ํ๊ณ , ๋ง์ง๋ง์๋ ์ต์ข ์ฐ์น์๋ฅผ ์ถ๋ ฅํ๋๋ก ํ์๋ค.
// App.tsx
const App = () => {
return (
<Container>
<form onSubmit={onSubmit}>
{/*...*/}
</form>
<Processes process={process} />
<Result result={result} />
</Container>
)
์ ์ฒด ์ปจํ ์ธ ๋ ์ด์์์ ๊ตฌ์ฑํ๋ App.tsx ์๋ ์ฌ์ฉ์์๊ฒ ์ ๋ ฅ์ ๋ฐ๋ form ์๋ฆฌ๋จผํธ, ๊ฒฐ๊ณผ๋ฅผ ์ถ๋ ฅํ๋ Processes ์ปดํฌ๋ํธ, ๊ทธ๋ฆฌ๊ณ ๊ฒฐ๊ณผ๋ฅผ ์ถ๋ ฅํ๋ Result ์ปดํฌ๋ํธ ์ด 3๊ฐ๋ก ๊ตฌ์ฑํ์๋ค.
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 ๋ฅผ ์ฌ์ฉํ๋ค.
์ ํจ์ฑ ๊ฒ์ฌ๋ 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]
)
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
}
}
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)
์๋์ฐจ ์ด๋์ ๋ํ ์ถ๋ ฅ๊ณผ ๊ฒฐ๊ณผ ์ถ๋ ฅ์ 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)
}
// racingCarModule
{
moveCars : '์๋์ฐจ๋ค ์ด๋',
moveCar : '์๋์ฐจ ๊ฐ์ฒด ํ๋ ์ด๋',
checkMoveCarCondition : '์๋์ฐจ๊ฐ ์ด๋ํ ์ง๋ง์ง ์กฐ๊ฑด์ฒ๋ฆฌ',
makeProcess : '์๋์ฐจ ์ด๋๊ฑฐ๋ฆฌ์ ๋ํ ์ถ๋ ฅ(jsx) ๋ฆฌํด',
makeDistance : '์๋์ฐจ ์ด๋๊ฑฐ๋ฆฌ(-) ๋ฌธ์์ด ๋ฆฌํด',
makeResult : '์ต์ข
๊ฒฐ๊ณผ์ ๋ํ ์ถ๋ ฅ(jsx) ๋ฆฌํด',
getWinner : 'position๊ฐ์ด ๊ฐ์ฅ ํฐ ์๋์ฐจ ๋ฆฌํด',
}
์ด๋ฐ ์์ผ๋ก, ํจ์๋ ์๊ธฐ ๊ธฐ๋ฅ๋ง ์ํํ ์ ์๋๋ก ์ต๋ํ ๋ง์ด ์ชผ๊ฐ์ฃผ์๋ค.
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)
})
})
ํ ์คํธ๋ ์์ง ์ต์์ง ์๋ ๊ฒ ๊ฐ๋ค. ์ข ๋ ํ์ตํ ๊ฒ! ํ ์คํธ ์์ฑ๊ท์น ๋ฐ ํ
์ฌ์ฉ์๊ฐ submit
์ ์ฐ์์ ์ผ๋ก ์ํํ์ ๋, ๊ธฐ์กด์ timer๋ค๋ ๊ฐ์ด ์ค์ฒฉ๋์ด ์คํ๋๋ฉฐ, ๋ทฐ๋ฅผ ๊ฒน์ณ์ ์
๋ฐ์ดํธํ๋ ์ค๋ฅ๊ฐ ์๋ค. ์
๋ ฅ ์ด๋ฒคํธ๋ฅผ ์ ํํ๊ฑฐ๋ timer๋ฅผ ์ฌ์ค์ ํด์ฃผ๋ ๋ฑ์ ์์
์ด ํ์ํ ๊ฒ ๊ฐ๋ค.