시작하는 중
리액트의 개념적 모델 본문
https://github.com/reactjs/react-basic
리액트의 작동방식
리액트의 핵심 전제
웹 페이지의 UI는 단순히 데이터를 다른 형태의 데이터로 반영한 것에 불과하기에 순수 함수로써 동일한 입력이라면 동일한 출력을 가진다는 전제가 깔려 있다.
function NameBox(name) {
return { fontWeight: 'bold', labelContent: name };
}
추상화
하지만 복잡한 UI를 하나의 함수에 담을 수는 없다. UI를 구현 세부 정보가 유출되지 않는 재사용 가능한 조각으로 추상화하는 것이 중요하다. 예를 들어 다른 함수에서 한 함수를 호출하는 것이다.
-> 여러 언어들은 객체지향을 통해 추상화를 하며 하나의 함수에 모든 로직을 담으려는 리스크를 지려고 하지 않는다. 추상화를 통해 UI를 구성하기 위한 함수를 재사용 가능한 조각으로 만들면서 구현 세부 정보를 유출시키지 않으려는 것
function FancyUserBox(user) {
return {
borderStyle: '1px solid blue',
childContent: [
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]
};
}
컴포지션
진정으로 재사용 가능한 기능을 구현하려면 단순히 리프를 재사용하고 이를 위한 새로운 컨테이너를 만드는 것만으로는 충분하지 않습니다. 또한 다른 추상화를 구성하는 컨테이너에서 추상화를 구축할 수 있어야 합니다. 제가 생각하는 '구성'은 두 개 이상의 서로 다른 추상화를 새로운 추상화로 결합하는 것입니다.
-> 단순하게 추상화를 한다고 해서 재사용 가능한 함수들을 만드는 것은 아니다. 추상화한 한 블록은 다른 블록을 추상화하기 위해 사용될 수 있어야 한다. sebmarkbage가 생각하는 "컴포지션"은 두 개 이상의 서로 다른 추상화를 새로운 추상화로 결합하는 것.
function FancyBox(children) {
return {
borderStyle: '1px solid blue',
children: children
};
}
function UserBox(user) {
return FancyBox([
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]);
}
상태
UI는 단순히 서버/비즈니스 로직 상태의 복제가 아닙니다. 실제로 특정 프로젝션에만 해당되고 다른 프로젝션에는 해당되지 않는 상태가 많이 있습니다. 예를 들어 텍스트 필드에 입력을 시작한다고 가정해 보겠습니다. 이 입력은 다른 탭이나 모바일 디바이스에 복제될 수도 있고 복제되지 않을 수도 있습니다. 스크롤 위치는 여러 프로젝션에 걸쳐 거의 복제하고 싶지 않은 대표적인 예입니다.
우리는 데이터 모델이 불변인 것을 선호하는 경향이 있습니다. 상단의 단일 원자처럼 상태를 업데이트할 수 있는 함수를 스레드합니다.
-> 마지막 줄이 중요한 것 같다. 리액트 팀은 데이터 모델이 불변인 것을 선호하기에, 상단(FancyNameBox의 단일 원자(최소한의 추상화 된 개념)처럼 상태를 업데이트할 수 있는 함수(addOneMoreLike())를 스레드(엮는것)한다.
function FancyNameBox(user, likes, onClick) {
return FancyBox([
'Name: ', NameBox(user.firstName + ' ' + user.lastName),
'Likes: ', LikeBox(likes),
LikeButton(onClick)
]);
}
// Implementation Details
var likes = 0;
function addOneMoreLike() {
likes++;
rerender();
}
// Init
FancyNameBox(
{ firstName: 'Sebastian', lastName: 'Markbåge' },
likes,
addOneMoreLike
);
코드를 보면, 클로저와 추상화 모두 사용하고 있다. 정말 멋진 코드인 것 같다.
메모이제이션
함수가 순수하다는 것을 알고 있다면 같은 함수를 반복해서 호출하는 것은 낭비입니다. 마지막 인수와 마지막 결과를 추적하는 메모화된 버전의 함수를 만들 수 있습니다. 이렇게 하면 같은 값을 계속 사용하더라도 함수를 다시 실행할 필요가 없습니다.
-> 함수가 순수(잘 추상화된 상태)하기에 이를 계속해서 호출할 필요는 없다. 따라서, 마지막 인수와 마지막 결과만을 추적하는 메모이제이션된 함수를 만들 수 있다. 메모이제이션을 통해 같은 값이라면 다시 실행하는 낭비를 막을 수 있다.
function memoize(fn) {
var cachedArg;
var cachedResult;
return function(arg) {
if (cachedArg === arg) {
return cachedResult;
}
cachedArg = arg;
cachedResult = fn(arg);
return cachedResult;
};
}
var MemoizedNameBox = memoize(NameBox);
function NameAndAgeBox(user, currentTime) {
return FancyBox([
'Name: ',
MemoizedNameBox(user.firstName + ' ' + user.lastName),
'Age in milliseconds: ',
currentTime - user.dateOfBirth
]);
}
목록
대부분의 UI는 목록의 일부 형태로 목록의 각 항목에 대해 여러 가지 다른 값을 생성합니다. 이렇게 하면 자연스러운 계층 구조가 만들어집니다.
목록의 각 항목에 대한 상태를 관리하려면 특정 항목의 상태를 포함하는 맵을 만들 수 있습니다.
-> UI를 목록화하여 관리한다는 것
function UserList(users, likesPerUser, updateUserLikes) {
return users.map(user => FancyNameBox(
user,
likesPerUser.get(user.id),
() => updateUserLikes(user.id, likesPerUser.get(user.id) + 1)
));
}
var likesPerUser = new Map();
function updateUserLikes(id, likeCount) {
likesPerUser.set(id, likeCount);
rerender();
}
UserList(data.users, likesPerUser, updateUserLikes);
user를 관리하는 UserList를 봐야한다.
FancyNameBox에 어떤 것들이 들어가는지 다시 보면..
function FancyNameBox(user, likes, onClick) {
return FancyBox([
'Name: ', NameBox(user.firstName + ' ' + user.lastName),
'Likes: ', LikeBox(likes),
LikeButton(onClick)
]);
}
user 하나와, likes, onClick함수가 들어간다.
1. UserList의 return 함수에서, user 객체 하나,
2. 그 사람을 좋아요한 사람의 수를 나타내는 map 객체,
3. 좋아요를 누르면 그 user의 id를 찾아내고 할당된 값을 + 1을 해주고 리렌더링을 해주는 updateUserLikes
이렇게 3개를 넘긴다.
다시 감탄하게 된다. 어떻게 이렇게 짤 수가 있는걸까?
continuation
안타깝게도 UI에는 목록이 너무 많기 때문에 이를 명시적으로 관리하려면 많은 상용구가 필요하게 됩니다.
함수 실행을 지연시킴으로써 이러한 상용구 중 일부를 중요한 비즈니스 로직에서 제거할 수 있습니다. 예를 들어, "커링"JavaScript에서 바인딩을 사용하면 됩니다. 그런 다음 보일러플레이트가 없는 핵심 함수 외부에서 상태를 전달합니다.
이렇게 하면 보일러플레이트를 줄이지는 못하지만 적어도 중요한 비즈니스 로직에서 벗어나게 됩니다.
-> 밑의 과정을 따라오면 알겠지만, UserList안의 borderStyle가 "보일러 플레이트"이기에 밖으로 빼려고 노력한다.
바인드가 무엇인지부터 알아야겠는데 참고한 링크를 통해서 설명을 대체하고자 한다.
https://ko.javascript.info/bind
링크의 코드를 위해선, 인자를 받아오는 부분만 보면 된다.
약간 클로저와 유사한 것 같다.
function FancyUserList(users) {
return FancyBox(
UserList.bind(null, users)
);
}
const box = FancyUserList(data.users);
const resolvedChildren = box.children(likesPerUser, updateUserLikes);
const resolvedBox = {
...box,
children: resolvedChildren
};
box는 UserList를 this는 null이고 받을 수 있는 추가 인자로 users 하나가 바인딩된 결과가 할당된다.
function FancyBox(children) {
return {
borderStyle: '1px solid blue',
children: children
};
}
결과적으로 box는 FancyBox의 children에 UserList(users, ?, ?)가 바인딩된 상태이다.
resolvedChildren는 box의 children에 box의 UserList(users, likesPerUser, updateUserLikes)의 결과가 할당된 상태이다.
resolvedBox는
{
borderStyle: '1px solid blue',
children: [user의 정보가 담긴 배열]
}
이 된다.
보일러 플레이트인 borderStyle : "1px solid blue"가 밖으로 나온 모습
상태 맵
앞서 반복되는 패턴을 발견하면 컴포지션을 사용하여 동일한 패턴을 반복해서 구현하지 않도록 할 수 있다는 것을 알고 있습니다. 상태를 추출하고 전달하는 로직을 자주 재사용하는 저수준 함수로 옮길 수 있습니다.
-> 앞에서 UserList안의 borderStyle를 밖으로 뺀 것처럼, state를 추출하여 저수준 함수로 옮길 수 있다.
function FancyBoxWithState(
children,
stateMap,
updateState
) {
return FancyBox(
children.map(child => child.continuation(
stateMap.get(child.key),
updateState
))
);
}
function UserList(users) {
return users.map(user => {
continuation: FancyNameBox.bind(null, user),
key: user.id
});
}
function FancyUserList(users) {
return FancyBoxWithState.bind(null,
UserList(users)
);
}
const continuation = FancyUserList(data.users);
continuation(likesPerUser, updateUserLikes);
continuation = [
{
borderStyle: '1px solid blue',
children : [
'Name: ',
{
fontWeight: 'bold',
labelContent: data.users[0].firstName + ' ' + data.users[0].lastName
}
'Likes: ', LikeBox(likesPerUser),
LikeButton(updateUserLikes)
]
},
...
]
메모이제이션 맵
목록에 있는 여러 항목을 메모하고 싶을 때는 메모하기가 훨씬 더 어려워집니다. 메모리 사용량과 빈도의 균형을 맞추는 복잡한 캐싱 알고리즘을 찾아야 합니다.
다행히도 UI는 같은 위치에서 상당히 안정적인 경향이 있습니다. 트리에서 같은 위치는 매번 같은 값을 가져옵니다. 이 트리는 메모화에 매우 유용한 전략으로 밝혀졌습니다.
상태에 사용한 것과 동일한 트릭을 사용하여 컴포저블 함수를 통해 메모화 캐시를 전달할 수 있습니다.
function memoize(fn) {
return function(arg, memoizationCache) {
if (memoizationCache.arg === arg) {
return memoizationCache.result;
}
const result = fn(arg);
memoizationCache.arg = arg;
memoizationCache.result = result;
return result;
};
}
function FancyBoxWithState(
children,
stateMap,
updateState,
memoizationCache
) {
return FancyBox(
children.map(child => child.continuation(
stateMap.get(child.key),
updateState,
memoizationCache.get(child.key)
))
);
}
const MemoizedFancyNameBox = memoize(FancyNameBox);
특별한 건 없는듯?? 리액트에서 쓰이는 클로저를 알고 있다면 그냥 해석할 수 있다.
Algebraic Effects
필요한 모든 작은 값을 여러 단계의 추상화를 통해 전달하는 것은 다소 번거로운 일이라는 것이 밝혀졌습니다. 중간 단계를 거치지 않고 두 추상화 사이에 무언가를 전달할 수 있는 지름길이 있으면 좋을 때가 있습니다. React에서는 이를 "컨텍스트"라고 부릅니다.
때때로 데이터 종속성이 추상화 트리를 깔끔하게 따르지 않을 때가 있습니다. 예를 들어 레이아웃 알고리즘에서는 자식들의 위치를 완전히 채우기 전에 자식들의 크기에 대해 알아야 합니다.
이제 이 예제는 약간 "바깥"에 있습니다. ECMAScript에 제안된 대로 대수 효과를 사용하겠습니다. 함수형 프로그래밍에 익숙하다면 모나드에 의해 부과된 중간 의식을 피하고 있습니다.
-> useContext가 props 체이닝을 건너뛰고 다른 컴포넌트의 상태를 참조할 수 있는 것처럼, react의 작동에서도 이와 같은 "Context"개념이 존재한다.
function ThemeBorderColorRequest() { }
function FancyBox(children) {
const color = raise new ThemeBorderColorRequest();
return {
borderWidth: '1px',
borderColor: color,
children: children
};
}
function BlueTheme(children) {
return try {
children();
} catch effect ThemeBorderColorRequest -> [, continuation] {
continuation('blue');
}
}
function App(data) {
return BlueTheme(
FancyUserList.bind(null, data.users)
);
}
raise new ThemeBorderColorRequest()와 try catch 를 통해서 이를 구현한 모습이다.
리액트를 어떻게 프로그래밍하였는지 볼 수 있는 글이 었다. 리액트에 대해 이해하는데 어느정도 도움이 되었지만, 이를 활용할 순 있을지는 의문이다..
근데, 멋진 코드와 로직을 볼 수 있어서 좋았다.
내가 했던 모듈화라는 것은 단순하게 함수와 변수를 따로 빼는 것 정도였는데, 진짜 추상화가 뭔지 배워볼 수 있었다.. 이래서 좋은 기업가서 많이 배워야 하나보다
reference
'react' 카테고리의 다른 글
DOM과 React의 가상 돔 (0) | 2023.03.22 |
---|---|
리액트 파이버 (0) | 2023.03.21 |
리액트 UI 트리 (0) | 2023.03.17 |
리액트에서의 클로저 (0) | 2023.03.15 |
React Side Effect와 useEffect (0) | 2023.01.17 |