ㅇㅇㅈ Blog

프론트엔드 수행중

0%

Structural Type System vs Nominal Type System

Structural Type System - 구조가 같으면, 같은 타입이다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Iperson {
name: string
age: number
speak(): string;
}

type PersonType = {
name: string
age: number
speak(): string;
}
// Iperson 과 PersonType은 똑같은 구조다

let personInterFace: IPerson = {} as any
let personType: PersonType = {} as any

personInterface = personType;
personType = personInterface

nominal type system - 구조가 같아도 이름이 다르면, 다른 타입이다

  • nominal type system은 타입스크립트는 따르지 않는다
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    type PersonID = string  & { readonly brand: unique symbol}

    function PersonID(id: string): PersinID{
    return id as PersonID
    }

    function getPersonById(id: PersonID){}

    getPersonbyId(personId('id-aaaa'))
    getPersonbyId(('id-aaaa')) // error TS2345: Argument of type 'string' is not assignable to parameter of type 'PersonID'. Type 'string' is not assignable to type '{ readonly brand: unique symbol}'

작성자와 사용자의 관점으로 코드 바라보기

타입이란 해당 변수가 할 수 있는 일을 결정한다

1
2
3
4
5
6
7
8
9
10
11
12
13
// JavaScript
// f1이라는 함수의 body에서는 a를 사용한다
// a가 할 수 있는 일은 a의 타입이 결정한다
function f1(a){
return a
}

// 사용자는 사용법을 숙지하지 않은 채, 문자열을 사용하여 함수를 실행
function f1(a){
return a * 38
}
console.log(f2(10)) // 380
console.log(f2('Mark')) // NaN

타입스크립트의 추론에 의지하는 경우

1
2
3
4
5
6
7
8
9
10
// 타입스크립트 코드지만
// a의 타입을 명시적으로 지정하지 않은 경우이기 때문에 a는 any로 추론된다
// 함수의 리턴타입은 number로 추론된다(NaN도 number이기 때문에)

function f3(a){
return a * 38
}
// 사용자는 a가 any이기 때문에, 사용법에 맞게 문자열을 사용하여 함수를 실행
console.log(f3(10)) // 380
console.log(f3('Mark')) // NaN

nolmplicitAny 옵션

타입을 명시적으로 지정하지 않은경우, 타입스크립트가 추론 중 ‘any’라고 판단하게 되면,
컴파일 에러를 발생시켜 명시적으로 지정하도록 유도한다

noImplicitAny 에 의한 방어

1
2
3
4
5
6
7
8
9
// error TS7006: Parameter 'a' implicitly has an 'any' type

function f3(a){
return a * 38;
}
// 사용자의 코드를 실행할 수 없습니다. 컴파일이 정상적으로 마무리 될 수 있도록 수정해야 합니다.
console.lof(f3(10))
console.log(f3('Mark') + 5)

number타입으로 추론된 리턴 타입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 매개변수의 타입은 명시적으로 지정
// 명시적으로 지정하지 않은 함수의 리턴 타입은 number로 추론된다

function f4(a: number){
if(a>0){
return a * 38
}
}

// 사용자는 사용법에 맞게 숫자형을 사용하여 함수를 실행했습니다.
// 해당 함수의 리턴 타입은 number이기 때문에, 타입에 따르면 연산을 바로 할 수 있다
// 하지만 실제 undefined + 5가 실행되어 NaN이 출력된다

console.log(f4(5)) // 190
console.log(f4(-5) + 5) // NaN

strictNullChecks 옵션

  • 모든 타입에 자동으로 포함되어 있는 ‘null’과 ‘undefined’를 제거해준다

number | undefined 타입으로 추론된 리턴 타입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 매개변수의 타입은 명시적으로 지정
// 명시적으로 지정하지 않은 함수의 리턴 타입은 number | undefined로 추론

function f4(a: number){
if(a>0){
return a * 38;
}
}

// 사용자는 사용법에 맞게 숫자형을 사용하여 함수를 실행
// 해당 함수의 리턴 타입은 number | undefined 이기 때문에,
// 타입에 따르면 이어진 연산을 바로 할 수 없다
// 컴파일 에러를 고쳐야하기 때문에 사용자와 작성자가 의논을 해야한다

console.log(f4(5))
console.log(f4(-5) + 5) // error TS2532: Object is possibly 'undefined'

명시적으로 리턴타입을 지정해야 할까?

1
2
3
4
5
6
7
8
9
// 매개변수의 타입과 함수의 리턴 타입을 명시적으로 지정했습니다.
// 실제 함수 구현부의 리턴 타입과 명시적으로 지정한 타입이 일치하지 않아 컴파일 에러가 발생한다

// error TS2366
function f5(a: number): number {
if( a > 0){ // if만 리턴 되기 때문에 if가 아닌 부분에 대한 작업이 필요함
return a * 38
}
}
  • noImplicitReturns 옵션을 켜면
    • 함수내에서 모든 코드가 값을 리턴하지 않으면, 컴파일 에러를 발생 시킨다
1
2
3
4
5
6
7
8
// if가 아닌 경우 return을 직접하지 않고 코드가 종료된다

// error TS7030: Not all code paths return a value.
function f5(a: number){
if( a > 0){
return a * 38;
}
}
Read more »

컴포넌트 추출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

function formatDate(date) {
return date.toLocaleDateString();
}

function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name} />
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}

const comment = {
date: new Date(),
text: 'I hope you enjoy learning React!',
author: {
name: 'Hello Kitty',
avatarUrl: 'http://placekitten.com/g/64/64'
}
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Comment
date={comment.date}
text={comment.text}
author={comment.author} />
);

  • 위 컴포넌트는 구성요소들이 모두 중첩 구조로 이루어져 있어서 변경하기 어려울수 있다.
  • 구성요소를 개별적으로 재사용하기도 힘들다
  • 구성요소를 컴포넌트로 추출한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React from 'react'
function formatDate(date) {
return date.toLocaleDateString()
}
const Avatar = (props) => {
return (
<img className='Avatar' src={props.user.avatarUrl} alt={props.user.name} />
)
}
const UserInfo = (props) => {
return (
<div className='UserInfo'>
<Avatar user={props.user} />
<div className='UserInfo-name'>{props.user.name}</div>
</div>
)
}
function Comment(props) {
return (
<div className='Comment'>
<UserInfo user={props.author} />
<div className='Comment-text'>{props.text}</div>
<div className='Comment-date'>{formatDate(props.date)}</div>
</div>
)
}

const comment = {
date: new Date(),
text: 'I hope you enjoy learning React!',
author: {
name: 'Hello Kitty',
avatarUrl: 'http://placekitten.com/g/64/64',
},
}

export default function Extraction() {
return (
<Comment date={comment.date} text={comment.text} author={comment.author} />
)
}

  • props의 흐름을 잘 파악해야한다
  1. Extraction 컴포넌트에서 Comment 컴포넌트 props로 date,text,author라는 이름으로 넘겨주고 있다
1
2
3
4
5
6
7
8
9
10
const Comment = (props) => {
console.log(props)
return (
<div className='Comment'>
<UserInfo user={props.author} />
<div className='Comment-text'>{props.text}</div>
<div className='Comment-date'>{formatDate(props.date)}</div>
</div>
)
}

Comment 컴포넌트의 props를 콘솔로 찍었을때

  1. Comment 컴포넌트에서 UserInfo 컴포넌트로 user라는 이름으로 {author}를 넘겨 주고 있다

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const UserInfo = (props) => {
    console.log(props)
    return (
    <div className='UserInfo'>
    <Avatar user={props.user} />
    <div className='UserInfo-name'>{props.user.name}</div>
    </div>
    )
    }

    UserInfo 컴포넌트의 props를 콘솔로 찍었을 때

  2. UserInfo 컴포넌트에서 Avatar 컴포넌트로 user라는 이름으로 {props.user} = {author} 를 넘겨 주고 있다

    1
    2
    3
    4
    5
    6
    const Avatar = (props) => {
    console.log(props)
    return (
    <img className='Avatar' src={props.user.avatarUrl} alt={props.user.name} />
    )
    }

    Avatar 컴포넌트에서 props를 찍었을때

React 상태 끌어올리기

형제 컴포넌트끼리 현재 상태를 알기 위해 부모 컴포넌트를 이용한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const root = document.querySelector('#root')
const IdInput = () => {
const [id, setId] = React.useState('')
const handleIdClick = (event) => {
setId(event.target.value)
}
return (
<>
<label htmlFor="id-input">ID : </label>
<input type="text" id="id-input" onChange={handleIdClick} />
</>
)
}
const PwInput = () => {
const [pw, setPW] = React.useState('')
const handlePwClick = (event) => {
setPW(event.target.value)
}
return (
<>
<label htmlFor="pw-input">PW : </label>
<input type="password" id="pw-input" onChange={handlePwClick} />
</>
)
}
function App() {
const handleLoginClick = () => {
alert(`${id} , ${pw}`)
}
return (
<>
<IdInput />
<PwInput />
<button disabled onClick={handleLoginClick}>Submit</button>
</>
);
}

ReactDOM.render(<App />, root)

위 코드에서는 button 에서 id의 상태와 pw의 상태를 알 수가 없다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const root = document.querySelector('#root')
const IdInput = ({ handleIdClick }) => {

return (
<>
<label htmlFor="id-input">ID : </label>
<input type="text" id="id-input" onChange={handleIdClick} />
</>
)
}
const PwInput = ({ handlePwClick }) => {

return (
<>
<label htmlFor="pw-input">PW : </label>
<input type="password" id="pw-input" onChange={handlePwClick} />
</>
)
}
function App() {
const [id, setId] = React.useState('')
const handleIdClick = (event) => {
setId(event.target.value)
}
const [pw, setPW] = React.useState('')
const handlePwClick = (event) => {
setPW(event.target.value)
}
const handleLoginClick = () => {
alert(`${id} , ${pw}`)
}
return (
<>
<IdInput handleIdClick={handleIdClick} />
<PwInput handlePwClick={handlePwClick} />
<button disabled={id.length === 0 || pw.length === 0} onClick={handleLoginClick}>Submit</button>
</>
);
}

ReactDOM.render(<App />, root)

lifting up

  • useState를 형제간의 가장 가까운 부모 컴포넌트인 App에 준다
  • 컴포넌트의 props로 변경된 상태를 넘겨준다
  • 과도한 lifting은 drilling을 야기한다

Key 와 리렌더링

Key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕습니다. key는 엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const todos = [
[
{ id: 1, value: 'Wash dishes' },
{ id: 2, value: 'Clean the bed' },
{ id: 3, value: 'Running' },
{ id: 4, value: 'Learning' }
],
[
{ id: 4, value: 'Learning' },
{ id: 1, value: 'Wash dishes' },
{ id: 2, value: 'Clean the bed' },
{ id: 3, value: 'Running' },
]
,
[
{ id: 2, value: 'Clean the bed' },
{ id: 4, value: 'Learning' },
{ id: 1, value: 'Wash dishes' },
{ id: 3, value: 'Running' },
]
,
[
{ id: 2, value: 'Clean the bed' },
{ id: 3, value: 'Running' },
{ id: 4, value: 'Learning' },
{ id: 1, value: 'Wash dishes' },
]
]

function App() {
const [items, setItems] = React.useState(todos[0])
React.useEffect(() => {
const interval = setInterval(() => {
const random = Math.floor(Math.random() * 3)
setItems(todos[random])
}, 1000)
return () => {
clearInterval(interval)
}
}, [])
const handleDoneClick = (todo) => {
setItems(items => items.filter(item => item !== todo))
}
const handleRestoreClick = () => {
setItems(items => [...items, todos.find(item => !items.includes(item))])
}
return (
<>
{items.map((todo,index) => (
<div key={todo.id}>
<button onClick={() => handleDoneClick(todo)}>{todo.value}</button>
</div>
))}
<br />
<br />
<button onClick={handleRestoreClick}>Restore</button>
</>
);
}

ReactDOM.render(<App />, root)
  • Key를 선택하는 가장 좋은 방법은 리스트의 다른 항목들 사이에서 해당 항목을 고유하게 식별할 수 있는 문자열을 사용하는 것입니다. 대부분의 경우 데이터의 ID를 key로 사용합니다.
  • 항목의 순서가 바뀔 수 있는 경우 key에 인덱스를 사용하는 것은 권장하지 않는다. 이로 인해 성능이 저하되거나 컴포넌트의 state와 관련된 문제가 발생할 수 있다.
  • 리스트 항목에 명시적으로 key를 지정하지 않으면 React는 기본적으로 인덱스를 key로 사용한다

Element.getBoundingClientRect()

Element.getBoundingClientRect() 는 엘리먼트의 크기와 뷰포트에 상대적인 위치 정보를 제공하는

DOMRect 객체를 반환

1
domRect = element.getBoundingClientRect()


반환 값은 padding과 border-width를 포함해 전체 엘리먼트가 들어 있는 가장 작은 사각형인 DOMRect 객체입니다. left, top, right, bottom, x, y, width, height 프로퍼티는 전반적인 사각형의 위치와 크기를 픽셀 단위로 나타냅니다. width와 height가 아닌 다른 프로퍼티는 뷰포트의 top-left에 상대적입니다.

React.useRef hook

ref로 DOM 다루기
리액트를 사용하는 프로젝트에서 가끔씩 DOM을 직접 선택해야 하는 상황이 발생한다.
그럴때 리액트에서 ref를 사용한다

Vanilla js -> document.get/ documnet.query
React -> useRef / ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const root = document.querySelector('#root')

function App() {
const inputRef = React.useRef()
React.useEffect(() => {
inputRef.current.focus()

return (
<React.Fragment>
<input ref={inputRef} />
</React.Fragment>
)
}
ReactDOM.render(<App />, root)
  1. useRef()를 inputRef 변수에 넣고, 실제 input에다 ref값으로 설정해준다
  2. ref객체의 .current 값은 원하는 DOM을 가르키게 된다

Array.prototype.shift()

  • shift 메서드는 원본 배열에서 첫 번째 요소를 제거하고 제거한 요소를 반환한다
  • 원본 배열이 빈 배열이면 undefined를 반환한다
  • 원본 배열이 변경 된다
1
2
3
4
5
6
7
8
const arr = [1,2]

// 원본 배열에서 첫 번째 요소를 제거하고 제거한 요소를 반환
let result = arr.shift()
console.log(result) // 1

// 원본 배열 변경됨
console.log(arr) // [2]

useEffect

1
2
3
React.useEffect(()=>{}, [deps])
// function : 수행하고자 하는 작업
// deps : 배열 형태이며 배열 안에는 감시하고자 하는 특정 값 or 빈 배열
  • 배열을 생략하면 렌더링이 발생할때 마다 실행된다
  • 컴포넌트가 화면에 가장 처음 렌더링 될때 한번만 실행하고 싶을 떄는 deps 위치에 빈 배열을 넣는다
  • 배열안에 값이 있으면 특정 값이 바뀔때만 실행된다

https://ko.reactjs.org/docs/hooks-effect.html#effects-without-cleanup

useEffect Hook을 componentDidMount와 componentDidUpdate, componentWillUnmount가 합쳐진 것으로 생각해도 좋습니다.

1
2
3
4
5
6
7
8
9
10
11
React.useEffect(()=>{
console.log('렌더링 될때마다 실행')
})

React.useEffect(()=>{
console.log('마운트 될 때만 실행된다')
}, [])

React.useEffect(()=>{
console.log('state가 변경 될때만 실행된다')
}, [state,state])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
const root = document.querySelector('#root')

function Child() {
console.log(' Child render start')
const [text, setText] = React.useState(() => {
console.log(' Child useState')
return 'Hi'
})
React.useEffect(() => {
console.log(' Child useEffect no deps')
return () => {
console.log(' Child CleanUp no deps')
}
})
React.useEffect(() => {
console.log(' Child useEffect empty deps')
return () => {
console.log(' Child CleanUp empty deps')
}
}, [])
React.useEffect(() => {
console.log(' Child useEffect [text] deps')
return () => {
console.log(' Child CleanUp [text] deps')
}
}, [text])
function changeHandler(e) {
setText((cur) => {
return cur = e.target.value
})
}
const element = (
<>
<input onChange={changeHandler}></input>
<p>{text}</p>
</>
)
console.log(' Child render end')
return element
}
const App = () => {
console.log('App render start ')
const [show, setShow] = React.useState(() => {
console.log("App useState")
return false
})
React.useEffect(() => {
console.log("App uesEffect, [show] deps")
return () => {
console.log("App CleanUp, [show] deps")
}
}, [show])
React.useEffect(() => {
console.log("App uesEffect, empty deps")
return () => {
console.log("App CleanUp, empty deps")
}
}, [])
React.useEffect(() => {
console.log("App uesEffect, no deps")
return () => {
console.log("App CleanUp, no deps")
}
})
function handleClick() {
setShow(cur => !cur)
}
console.log('App render end ')
return (
<>
<button onClick={handleClick}>search</button>
{show ? (
<Child />
) : null}

</>
)
}
ReactDOM.render(<App />, root)

콘솔로 hook의 호출 시점을 찍어본다

1. 첫 렌더시

1. App이 렌더
2. useState 실행 
3. 렌더종료 후에 useEffect가 호출된다

2. App 컴포넌트에 변경이 생겼을때

1. App 렌더 후에 렌더 종료
2. Child 컴포넌트 렌더 시작
3. Child의 useState 호출
4. Child 렌더 종료
5. 후에 App의 useEffect CleanUp 이 호출됨 이때 빈 배열[] 이 들어간 useEffect는 실행 되지 않는다.
6. Child의 useEffect 호출
7. 마지막에 App의 useEffect가 호출된다 이때도 빈 배열[] useEffect는 호출 안됨.

3. Child 컴포넌트에 변경이 생겼을때

1. Child 렌더 시작
2. Child 렌더 종료
3. Child useEffect cleanup 호출 빈배열은 호출 안됨
4. Child useEffect 호출 빈배열 호출 안됨

4. 이후 App 컴포넌트에 다시 변경이 생겼을때

1. App 렌더 시작
2. App 렌더 종료
3. Child useEffect cleanup 호출 > 전부 호출 된다
4. App useEffect cleanup 호출 > 빈 배열 호출 안됨
5. App useEffect 호출 > 빈 배열 호출 안됨

Read more »

컴포넌트 상태 다루기

DOM : 논리트리
컴포넌트 : 엘리먼트의 집합
엘리먼트 : 요소

useState hook

  • useState는 현재의 값을 저장하는 기능과 업데이트하는 함수를 쌍으로 제공한다.
  • 해당 함수는 이벤트 핸들러에서 호출이 가능하다.
  • 컴포넌트 안에서 useState는 여러 번 선언이 가능하다.
  • 또한 초기 값으로는 아까처럼 숫자뿐만 아니라 문자, 그리고 배열을 넣는 것도 가능하다.
1
2
const [state,setState] = React.useState(state)
// [상태 값 저장 변수,상태 갱신 함수] = React.useState(상태 초기값)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Btn() {
const [keyword, setKeyword] = React.useState('')
const [typing, setTyping] = React.useState(false)
const [result, setResult] = React.useState('')

function handleChange(e) {
setKeyword((cur) => cur = e.target.value)
setTyping(true)
}
function handleClick() {
setTyping(false)
setResult((cur)=> cur = `we find result of ${keyword}`)
}
return (
<>
<input type="text" onChange={handleChange} ></input>
<button onClick={handleClick}>search</button>
<p>{typing ? `Looking for ...${keyword}` : result}</p>
</>
)
}
const App = () => {
return (
<Btn />
)
}
ReactDOM.render(<App />, root)

상태 초기값을 가져올때 딜레이가 걸릴수도 있을경우 함수로 작성해 호출을 보장해 줄 수 있다

1
const [keyword, setKeyword] = React.useState(() => window.localStorage.getItem('keyword'))