React 스터디 10

React


React Study 10

  • 객체의 접근 방법
  • YouTube 프로젝트 변경점
    • 좋아요, 싫어요 기능 추가
  • 댓글 추가 / 삭제 기능 추가
  • Stale closure problem
    • 결과를 변경하기 위한 해결점 1 : 함수를 만들고 함수로 호출
    • 결과를 변경하기 위한 해결점 2 : 즉시실행함수를 사용한다.
  • React Hook
    • useState
    • useEffect



용어 - ( 러버덕 )

  • 객체의 접근 방법
  • React Hook
  • useState
  • useEffect
  • Stale closure problem


객체의 접근 방법

1
2
3
4
5
6
7
8
9
10
const state = { id : 'abc', pwd : '1234' }
state.id; // abc
state['id'] // abc

cont pwd = 'pwd'
state[pwd] // 1234
state['pwd'] // 1234
state.pwd //1234

state.id === state[id]; // true

YouTube 프로젝트 변경점

좋아요, 싫어요 기능 추가

Action 구성하기

액션의 내용은 현재 좋아요와 싫어요를 기본적으로 저장할 id와 좋아요인지 싫어요인지 구별하기 위한 toggle_like를 구별한다.

1
2
3
4
5
export const LIKE = 'LIKE';

export function like(id, toggle_like) {
return ({type:LIKE, id, toggle_like})
}

Reducer 구성하기

Action 구성을 했다면 toggle_like를 구성해준다.

toggle은 true인지 false인지만 구분하면 되므로 if ... else로 구분해준다.

data && data.likeCount 에서 && 연산자의 의미는

첫번째, 자바스크립트에게 객체 내부로 접근하기 전에 상위 레벨부터 접근해야만 한다.

바로 data.likeCount로 접근했을 경우, data가 존재하지 않는다면,

undefined.likeCount가 될 수도 있기 때문이다.

두번째, 해당 소스는 likeCountdisLikeCount로 총 2개가 나뉘어져 있다.

만약 2개중 한개라도 사용되었다면 이미 data는 존재하는 것이기 때문에 항상 참이게 된다.

그럴경우 초기화가 되어있지 않은 data.likeCount는 null 상태인데 여기서 +1을 아무리 해도 NULL이다.

따라서 2개의 비교가 필요하다.

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
case LIKE :
if(action.toggle_like) {
return {
...state, // 쿼리를 보존하기위해서
data: {
...state.data, // 나중에 추가될 데이터를 보존하기위해서
[action.id]: {
...data, // Comment의 데이터를 보존하기 위해서 나열해주엇음.
likeCount: data && data.likeCount
? data.likeCount + 1
: 1,
}
}
}
} else {
return {
...state, // 쿼리를 보존하기위해서
data: {
...state.data, // 나중에 추가될 데이터를 보존하기위해서
[action.id]: {
...data, // Comment의 데이터를 보존하기 위해서 나열해주엇음.
disLikeCount: data && data.disLikeCount
? data.disLikeCount + -1
: -1,
}
}
}
};

VideoPlayer Redux 추가하기

VideoPlayer에 Redux를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<button onClick={() => props.like(_id, true)}>좋아요</button>
{ props.data[_id] &&
props.data[_id].likeCount ? props.data[_id].likeCount : 0}
<button onClick={() => props.like(_id, false)}>싫아요</button>
{ props.data[_id] &&
props.data[_id].disLikeCount ? props.data[_id].disLikeCount : 0}

...
};

function mapStateToProps(state) {
...
}

function mapDispatchToProps(dispatch) {
return bindActionCreators({
like,
}, dispatch)
}

export default connect(mapStateToProps,mapDispatchToProps)(VideoPlayer);

댓글 추가 / 삭제 기능 추가

Action 구성하기

댓글 추가 및 삭제기능 전부 id객체 내부따라서 저장이 되야하므로 2개를 전부 받는다.

댓글 추가 기능은 텍스트 값을 입력 받아야하므로 val 를 인자로 받고

댓글 삭제 기능은 각 댓글마다 있는 commentId 를 가지고 삭제해야 한다.

1
2
3
4
5
6
7
8
9
10
export const COMMENT = 'COMMENT';
export const DELETECOMMENT = 'DELETECOMMENT';

export function comment(id, val) {
return ({type:COMMENT, id, val})
}

export function deleteComment(id, commentId) {
return ({type:DELETECOMMENT, id, commentId})
}

Reducer 구성하기

먼저 댓글 추가 기능을 작성하자.

댓글을 추가할 때 중복이 되어야 하므로 배열로 받는다.

data 객체와 comments 배열이 둘다 존재 하지 않을 경우

배열 내부에 객체로 text(댓글 내용)cid(댓글 아이디)를 추가한다.

여기서, cid(댓글 아이디)는 여러가지 방법이 있지만, 새로 추가된 댓글과 중복되어서는 안된다.

나는 여기서 1씩 늘어나는 방식을 사용한다.

cid : Math.max(-1, ...data.comments.map((id) => id.cid)) + 1

는 댓글이 존재할 경우 해당 댓글아이디만을 map()함수를 이용해 뽑아낸뒤 Math.max를 이용해 최고값을 갱신하다.

그 후 1을 더해준다.

여기서 Math.max(-1)을 기본으로 두어 만약 댓글이 전부 삭제하고 다시 추가할 경우, 다시 0부터 시작하게끔 설정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
case COMMENT :
return {
...state,
data : {
...state.data,
[action.id]: {
...data,
comments: data && data.comments
? [
{
text: action.val,
cid : Math.max(-1, ...data.comments.map((id) => id.cid)) + 1
},
...data.comments
]
: [{ text: action.val, cid : 0 }],
}
}
};

삭제기능은 VideoPlayer 에서 선택한 비디오의 cidfilter()를 돌리면서 뽑아내 cid 를 비교하여 삭제하는 방식을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
case DELETECOMMENT :
return {
...state,
data : {
[action.id]: {
comments: data && data.comments.filter((comment) => {
return comment.cid !== action.commentId
})
}
}
};

VideoPlayer Redux 추가하기

props.data[_id] && props.data[_id].comments && props.data[_id].comments.map((comment) 의 의미는

자바스크립트에서는 객체에 접근하기 위해서는 상위부터 접근해야만 한다.

props.data[_id].comments에 접근할 때, 만약 props.data[_id]가 없다면 javascript는 props.data[_id].comments

undefined.comments로 인식을 하기 때문이다. 따라서 방어코드 최상위부터 접근을 해야하는데 해당 객체의 최상위는 props다.

그런데 props는 Router 컴포넌트를 통해 뿌려지기 때문에 절대 없을 수가 없으므로 검사를 할 필요가 없다. 따라서 다음 단계인

props.data[_id]부터 검사하기 시작한다.

삭제는 선택한 요소의 cid를 보내주어야 하므로 해당 객체를 선택할 당시의 comment.cid를 클릭의 인자값으로 넘겨준다.

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
  const handleEnter = (k) => (e) => {
if(e.key === 'Enter') {
k(_id, e.target.value);
}
}

return (
...
<input type='text' onKeyPress={handleEnter(props.comment)} />
<div>
{
props.data[_id] &&
props.data[_id].comments &&
props.data[_id].comments.map((comment) =>
<h1 key={uuid.v4()}>
{comment.text}
<button onClick={() => props.deleteComment(_id, comment.cid)}>삭제</button>
</h1>
)
}
</div>
</div>
);
};

function mapStateToProps(state) {
...
}

function mapDispatchToProps(dispatch) {
return bindActionCreators({
like,
comment,
deleteComment
}, dispatch)
}

export default connect(mapStateToProps,mapDispatchToProps)(VideoPlayer);

Stale closure problem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function useState(init) {
let _val = init;

function setState(newVal) {
_val = newVal
}

return [_val, setState];
}

const [query, setQuery] = useState('여행')
console.log(query)
setQuery('크리스마스')
console.log(query)

// 여행, 여행

값을 리턴할 경우, 상위 렉시컬환경의 값이 업데이트 되지않는다.


결과를 변경하기 위한 해결점 1 : 함수를 만들고 함수로 호출

함수 내부에서만 작동하는 변수일 경우 앞에 _를 붙여주는 경우가 많다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function useState(init) {
let _val = init;

function useState () {
return _val
}

function setState(newVal) {
_val = newVal
}

return [useState, setState];
}

const [query, setQuery] = useState('여행')
console.log(query())
setQuery('크리스마스')
console.log(query())

// 여행, 크리스마스

결과를 변경하기 위한 해결점 2 : 즉시실행함수를 사용한다.

  • 외부에서 접근하지 못하도록 변수를 보호할 수 있다.
  • 기존의 함수에서 변수 리턴 시 업데이트가 되지 않는 문제를 해결.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const React = (function() {
let _val
return {
render(Component) {
const Comp = Component()
Comp.render()
return Comp
},
useState(init) {
_val = _val || init
function setState(newVal) {
_val = newVal
}
return [_val, setState]
}
}
})();

React Hook

리액트에서는 Hook을 IIFE(즉시실행함수) 형태로 폴리필 해보자.

많은 함수가 있게지만 useState는 다음과 같이 작성한다.

리액트 훅은 무조건 최상위에서만 사용한다.(for문, if문 등의 중첩 내에서 리액트 훅 사용 X)

리액트 훅 내부에서 for, if문등의 사용은 가능하다.

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
const React = (function() {
let _val
return {
render(Component) {
const Comp = Component()
Comp.render()
return Comp
},
useState(init) {
_val = _val || init
function setState(newVal) {
_val = newVal
}
return [_val, setState]
}
}
})()

const { useState } = React
function Counter() {
const [count, setCount] = useState(0)
return {
click : () => setCount(count + 1),
render : () => console.log('render', {count})
}
} // 개발자가 작성한 코드 : 리액트 훅

let App
App = React.render(Counter)
App.click()
App = React.render(Counter)

useState

1
2
3
4
5
6
7
8
const { useState } = React
function Counter() {
const [count, setCount] = useState(0)
return {
click : () => setCount(count + 1),
render : () => console.log('render', {count})
}
} // 개발자가 작성한 코드 : 리액트 훅

리액트 훅에서는, this.setState를 사용하지 않는다.

useState변수와 변수를 업데이트하기 위한 함수를 정의한다.


useEffect

componentDidMount, componentDidUpdate를 합쳐놓은 느낌.

componentDidUpdate와 동일하게 는 모든 변화에 대해 업데이트 하기 때문에 모니터 조건이 필요하다.

useEffect 역시 모니터 조건이 반드시 필요하다.

  • (옵션) 조건식으로 쓸 변수들을 Array로 작성시킨다.
1
2
3
4
5
6
7
8
9
10
11
useEffect(() => {
console.log(count);
}) // 기본 : 모든 업데이트에 대해 변화를 감지한다. ( 반드시 조건식을 작성해야만 한다. )

useEffect(() => {
console.log(count);
}, [count]) // 조건쓸 변수를 배열에 작성 : [count] 요소의 변화가 있을 경우에만 감지.

useEffect(() => {
console.log(count);
}, []) // 빈 배열 : 빈배열로 작성 시 단 한번만 실행된다.

Share