// bad functionhashIt(data) { // The hash let hash = 0;
// Length of string const length = data.length;
// Loop through every character in data for (let i = 0; i < length; i++) { // Get character code. const char = data.charCodeAt(i); // Make the hash hash = (hash << 5) - hash + char; // Convert to 32-bit integer hash &= hash; } }
// good functionhashIt(data) { let hash = 0; const length = data.length;
for (let i = 0; i < length; i++) { const char = data.charCodeAt(i); hash = (hash << 5) - hash + char;
// Convert to 32-bit integer hash &= hash; } }
주석 처리된 코드를 codebase에 남겨놓지 말고 지워버리기!
형상 관리 툴 (버전 관리; version control) 이 이런 코드들을 남겨놓기 위함이니, 오래된 코드는 옛 버전에 남겨놓자.
1 2 3 4 5 6 7 8
// bad doStuff(); // doOtherStuff(); // doSomeMoreStuff(); // doSoMuchStuff();
// good doStuff();
기록용 주석은 사용하지 말자
위와 동일하게, 형상 관리 툴 (git : git log 를 통해 이전 버전들 확인) 을 이용하고 필요없는 주석은 모두 날려버리자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// bad /** * 2016-12-20: Removed monads, didn't understand them (RM) * 2016-10-01: Improved using special monads (JP) * 2016-02-03: Removed type-checking (LI) * 2015-03-14: Added combine with type-checking (JR) */ functioncombine(a, b) { return a + b; }
// good functioncombine(a, b) { return a + b; }
위치 표시 주석도 피하자!
적절한 indentation, 함수명 및 변수명 , 포맷팅 을 이용한다면 기능 구분을 위한 정신없는 positional marker comments 는 필요하지 않다.
// bad describe("MomentJS", () => { // date boundary를 ☹️한 데 모아😡 처리함. it("handles date boundaries", () => { let date;
date = new MomentJS("1/1/2015"); date.addDays(30); assert.equal("1/31/2015", date);
date = new MomentJS("2/1/2016"); date.addDays(28); assert.equal("02/29/2016", date);
date = new MomentJS("2/1/2015"); date.addDays(28); assert.equal("03/01/2015", date); }); });
// good describe("MomentJS", () => { // date boundary handler를 😄여러 조건으로 나누어🤩 정의함. // 30일을 가진 month 의 경우 it("handles 30-day months", () => { const date = new MomentJS("1/1/2015"); date.addDays(30); assert.equal("1/31/2015", date); });
// 윤년의 경우 it("handles leap year", () => { const date = new MomentJS("2/1/2016"); date.addDays(28); assert.equal("02/29/2016", date); });
// 그 외, 31일을 가진 month && 윤년이 아닌 경우 it("handles non-leap year", () => { const date = new MomentJS("2/1/2015"); date.addDays(28); assert.equal("03/01/2015", date); }); });
classAjaxAdapterextendsAdapter{ constructor() { super(); this.name = "ajaxAdapter"; } // good // 즉, REFACTORING 을 잘한 것이 OCP를 적용한 것 request(url) { // request and return promise } }
classNodeAdapterextendsAdapter{ constructor() { super(); this.name = "nodeAdapter"; } // good request(url) { // request and return promise } }
// 3. 정사각형도 Shape 를 상속받지만, 직사각형과 다른 특징을 가지므로 직사각형과 'is-a' 관계로 정의 (=== 상속) 하지 않는다. classSquareextendsShape{ constructor(length) { super(); this.length = length; }
// 4. Shape 를 통해 속성값을 설정해놓아 이를 분리하여 동작을 할 수 있도록 한다. 이로써 여러 모듈 (클래스) 가 엉키지 않고 독립적으로 작용하여 에러를 줄일 수 있으며, 다른 속성을 가진 객체들을 유연하게 다룰 수 있다. functionrenderLargeShapes(shapes) { shapes.forEach(shape => { const area = shape.getArea(); shape.render(area); }); }
// 속성값 constructor의 인자로 전달 const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]; renderLargeShapes(shapes);
Interface Segregation Principle (ISP)
인터페이스 분리 원칙
자바스크립트는 인터페이스 (i.e. type) 가 존재하지 않기 때문에 해당 원칙이 명확하게 적용되지는 않지만, type 시스템이 존재하지 않음에도 불구하고 중요하게 작용하는 원칙이다.
“클라이언트는 그가 사용하지 않는 인터페이스에 의존하도록 강요받지 않아야 한다.”
자바스크립트는 duck typing 이므로 여기서 인터페이스는 암시적인 요소로 존재한다.
e.g. 큰 setting 객체들을 요구하는 클래스를 예시로 들면,
클라이언트는 대게 모든 setting을 필요로 하지는 않기 때문에, 많은 양의 option을 설정하도록 요구하는 것은 좋지 않다.
따라서, Setting을 optional하게 만드는 것은 “fat interface” 를 방지한다.
const $ = new DOMTraverser({ rootNode: document.getElementsByTagName("body"), animationModule() {} // Most of the time, we won't need to animate when traversing. // ... });
setup() { this.rootNode = this.settings.rootNode; // settings에 직접 접근하지 않고 setupOptions() 메소드를 이용한다. this.setupOptions(); }
// options를 세팅하는 메소드를 따로 작성한다. setupOptions() { // 이로써 options에 접근하도록 한다. // setter과 같은 역할 if (this.options.animationModule) { // ... } }
traverse() { // ... } }
const $ = new DOMTraverser({ rootNode: document.getElementsByTagName("body"), // 'Most of the time, we won't need to animate when traversing.' 이므로, // 이를 options로 설정하여 무조건 실행되지 않고 선택적으로 실행될 수 있도록 한다. options: { animationModule() {} } });
Dependency Inversion Principle (DIP)
의존성 역전 원칙
상위 모듈은 하위 모듈에 종속되어서는 안 된다. 둘 다 추상화에 의존해야 한다.
추상화는 세부사항에 의존하면 안 된다. 세부사항이 추상화에 의존해야 한다.
의존성 ( Coupling ) 이 높을수록 코드를 리팩토링하기 어렵다. 👉 나쁜 개발 습관
상위 모듈이 하위 모듈의 세부사항을 알지 못하므로, 의존성을 감소시킬 수 있다.
자바스크립트는 인터페이스가 없어 추상화에 의존한다는 것은 암시적인 것 (약속) 이므로, 다른 객체나 클래스에 노출되는 메소드와 속성 자체가 이에 (암시적인 약속, 추상화) 해당한다고 볼 수 있다.
// 암시적인 약속 // InventoryTracker에 대한 모든 요청 모듈은 requestItems 메소드를 가질 것이다. requestItem(item) { // ... } } /* // Requester를 버전 별로 (e.g. V1, V2) 나누어서 클래스로 생성 가능하다. class InventoryRequesterV1 { constructor() { this.REQ_METHODS = ["HTTP"]; } requestItem(item) { // ... } } class InventoryRequesterV2 { constructor() { this.REQ_METHODS = ["WS"]; } requestItem(item) { // ... } } */
// bad classInventoryTracker{ constructor(items) { this.items = items;
// BAD: We have created a dependency on a 'specific' request implementation. // We should just have requestItems 'depend on a request method': `request` this.requester = new InventoryRequester(); }
// By constructing our dependencies externally and injecting them, we can easily // substitute our request module for a fancy new one that uses WebSockets. const inventoryTracker = new InventoryTracker( ["apples", "bananas"], // requester를 인자로 전달하여 tracker의 default requester로 객체를 설정하(여 코드를 정의하)는 것이 아니라 tracker 생성 시 개별적으로 적용 (생성) 할 수 있게 설정한다. new InventoryRequesterV2() ); inventoryTracker.requestItems();
// bad const Animal = function(age) { if (!(thisinstanceof Animal)) { thrownewError("Instantiate Animal with `new`"); }
this.age = age; };
// 함수 설정 Animal.prototype.move = functionmove() {};
// good classAnimal{ constructor(age) { this.age = age; }
move() { /* ... */ } }
/////////////// // bad const Mammal = function(age, furColor) { if (!(thisinstanceof Mammal)) { thrownewError("Instantiate Mammal with `new`"); }
// call 함수를 이용해 age 전달 Animal.call(this, age); this.furColor = furColor; };
Mammal.prototype = Object.create(Animal.prototype); Mammal.prototype.constructor = Mammal; // 함수 정의 Mammal.prototype.liveBirth = functionliveBirth() {};
// good classMammalextendsAnimal{ constructor(age, furColor) { // super() 을 이용해 상속 super(age); this.furColor = furColor; }
liveBirth() { /* ... */ } }
////////////// // bad const Human = function(age, furColor, languageSpoken) { if (!(thisinstanceof Human)) { thrownewError("Instantiate Human with `new`"); }
리렌더링 시 useState 가 반환하는 첫번째 값 (인자) 은 갱신된 최신 state 이다.
Note
React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
Functional Updates
함수적 갱신
setState 가 값을 갱신하는 과정을 직접 코드로 작성하면 다음과 같다.
1 2 3 4 5
const [state, setState] = useState({}); setState(prevState => { // Object.assign would also work return {...prevState, ...updatedValues}; });
갱신된 객체를 자동으로 merge하는 useState 와 반대로, setState 는 initialValue 를 유지한다.
이전 state 를 이용해서 값을 갱신하는 경우, 이전 값을 받아 갱신된 값을 반환하는 함수를 setState 로 전달하면 된다.
// 이전 값을 이용해서 값을 갱신하는 경우 // parameter value : 현재 값 // return value : 갱신할 값 <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> </> ); }
Note
Another option is useReducer, which is more suited for managing state objects that contain multiple sub-values.
업데이트 함수 (setState에 전달한 함수) 가 현재 상태와 정확히 동일한 값을 반환한다면 바로 뒤에 일어날 리렌더링은 완전히 건너뛰게 된다.
React에서 {} 를 쓰는 경우
JSX 내부에서는 객체를 받아옴
{{}} : 객체를 받아오는 것이 아니라 JSX 내부에서 생성할 때
JSX 외부에서는 일반 JS의 문법 중 function destructurizing을 실행
Lazy Initial State
지연 초기 state
initialState 인자는 초기 렌더링 시에 사용하는 state (값) 으로, 리렌더링 시 이 값은 무시된다.
만약 초기 state가 고비용 계산의 결과라면, 초기 렌더링 시에만 실행될 함수를 정의하여 전달하는 것이 효율적이다.
앞서 설명한 “업데이트 함수 (setState에 전달한 함수) 가 현재 상태와 정확히 동일한 값을 반환한다면 바로 뒤에 일어날 리렌더링은 완전히 건너뛰게 된다.” 를 추가적으로 설명하자면,
State Hook을 현재 state와 동일한 값으로 갱신 (update) 할 경우, 리액트는 children or firing effects를 렌더링하는 과정을 건너뛰고 실행 (처리) 을 종료한다.
Note
React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.
useEffect
함수형 컴포넌트 내에서는 React’s render phase에 따라 Mutations, subscriptions, timers, logging, and other side effects 가 허용되지 않는다.
이를 위해 useEffect 를 사용할 수 있다.
1
useEffect(didUpdate);
useEffect() : 모든 렌더링이 완료된 후 / 어떤 값이 변경되었을 때에만 동작 ( didUpdate ) 을 수행
didUpdate : 특정 effect를 발생시키는 (명령형) 함수
Cleaning Up an Effect
effect 정리
Effect는 컴포넌트가 화면에서 제거될 때 정리되어야 하는 요소 (e.g. subscription, timer ID 등) 를 만들곤 한다. 이를 정리하는 함수를 useEffect 에 전달할 수 있다.
1 2 3 4 5 6 7 8
useEffect(() => { // new subscription is created on every update const subscription = props.source.subscribe(); return() => { // Clean up the subscription subscription.unsubscribe(); }; });
clean-up function (e.g. subscription.unsubscribe() )은 컴포넌트가 제거되기(=== 다음의 effect) 이전 에 실행되어 memory leaks를 방지한다.
컴포넌트가 여러번 리렌더링 될 경우, 이전의 effect는 다음의 effect가 실행되기 이전 에 정리된다.
Timing of Effects
effect 타이밍
useEffect에 전달된 함수의 경우 레이아웃 배치과 그리기를 완료한 후, 지연된 이벤트가 발생하는 동안 실행된다. 이는 브라우저가 화면을 업데이트하는 과정을 방해하지 않기 때문에, 여러 side effects (e.g. setting up subscriptions and event handlers) 를 실행하기에 적합한 때이다.
useEffect 는 브라우저가 그려지기 이전까지는 대기하지만, 새로운 렌더링이 발생하기 이전에 실행되는 것을 보장한다. 리액트는 언제나 새로운 업데이트가 발생하기 이전에 이전의 렌더링 effect를 모두 제거한다.
useLayoutEffect
DOM 변경과 같이 사용자에게 effect가 보여지는 경우, visual inconsistency 방지를 위해 다음 execution (event) 이 실행되기 이전에 현재 화면이 렌더링이 됨과 동시에 effect가 (synchronously) 발생하여야 한다.
Conditionally Firing an Effect
조건부 effect 발생
Effect의 default behavior은 렌더링이 모두 완료된 이후 실행되는 것이다. 이는 해당 effect의 dependency 중 하나가 변경될 때마다 새로 실행됨을 의미한다.
앞선 예제를 통해서 자세히 보면,
1 2 3 4 5 6 7 8
useEffect(() => { // new subscription is created on every update const subscription = props.source.subscribe(); return() => { // Clean up the subscription subscription.unsubscribe(); }; });
subscription 은 매 업데이트마다 생성될 필요 없이, source prop 이 변할 때만 새로 생성되면 된다.
이와 같은 현상을 방지하기 위해, useEffect 의 2번째 argument로 effect가 의존하는 (depend on, dependency) 배열을 전달한다.
따라서 위 예제를 수정해보면,
1 2 3 4 5 6 7 8 9 10 11
useEffect( // 전달하는 함수는 동일하되, () => { const subscription = props.source.subscribe(); return() => { subscription.unsubscribe(); }; }, // 두번째 인자로 dependency array (depend on하는 배열) 을 전달한다. [props.source], );
이를 통해 subscription은 props.source가 변경될 때에만 재생성된다.
만약 effect를 (mount 또는 unmount 시) 한 번만 수행하고 싶다면 두번째 인자로 빈 배열 ( [] ) 을 전달하면 된다. 이를 통해 effect는 컴포넌트 범위에서 가져온 값들 (e.g. props, state 등) 에 전혀 의존하지 않으므로 해당 값들이 변경되어도 다시 실행되지 않는다.
빈 배열 ( [] ) 은 effect 안에 있는 props 와 state 가 항상 초기값을 가지게 됨을 의미한다. 이는 componentDidMount 와 componentWillUnMount 의 기능과 같다.
두번째 인자로 전달하는 배열은 effect에 사용되는 컴포넌트 범위의 모든 값 (e.g. props, state 등) 들을 포함해야 한다. 이를 위반할 시 이전 렌더링에서 설정한 값을 참조하는 버그가 발생한다.
이렇게 전달된 dependency 배열은 effect 함수에 인자로 전달되지 않는다. 하지만 effect 함수 내에서 참조된 모든 값들이 해당 배열에 포함되어야 하므로, 전달 여부는 유의미하지 않다.
useContext
1
const value = useContext(MyContext);
useContext() : ( React.createContext 를 통해 반환된 값인 ) context 객체에 **현재 context 값 (MyContext) **을 반환한다.
가장 가까운 상위 컴포넌트 <MyContext.Provider> 가 업데이트 되면, useContext 는 해당 컴포넌트에 전달 (포함) 된 가장 최신의 context value 에 따라 화면을 리렌더링 한다.
MyContext : 현재 context 값 (객체) 으로, 트리 구조에서 해당 컴포넌트의 상위 컴포넌트이면서 가장 가까운<MyContext.Provider> 의 value prop 에 따라 결정된다.
useContext() 의 인자는 context 객체여야 한다.
Correct:useContext(MyContext)
Incorrect:useContext(MyContext.Consumer)
Incorrect:useContext(MyContext.Provider)
useContext 를 사용한 컴포넌트는 context 값이 변경될 때마다 리렌더링된다. 리렌더링 되는 컴포넌트가 비싸다면, momoization 을 이용해 효율성을 높일 수 있다.
상위 컴포넌트에서 React.memo 나 shouldComponentUpdate 를 쓰더라도, useContext 를 사용한 컴포넌트부터 리렌더링이 진행 (시작) 된다.
useContext 는 context를 읽고 변경사항을 구독 (확인) 하는 것만 가능하기 때문에, 해당 context의 값을 전달하기 위해 트리 구조 내 상위 컴포넌트로서 <MyContext.Provider> 가 필요하다.
functionThemedButton() { // useContext의 인자로서 ThemeContext 객체 전달 // theme = <ThemeContext.Provider>의 value인 {themes.dark} // 객체 const theme = useContext(ThemeContext); return ( <button style={{ background: theme.background, color: theme.foreground }}> I am styled by theme context! </button> ); }
추가 Hooks
useReducer
useState() 의 대체재로서 사용한다. 여러 하위 값을 포함하거나 다음 state가 이전의 state 값에 영향을 받는 복잡한 state 로직을 가진 경우 선호된다.
컴포넌트의 state 업데이트 로직을 컴포넌트에서 분리하여 컴포넌트 바깥에서 작성하거나 다른 파일에서 작성한 후 불러와서 사용할 수도 있다.
또한 callback 대신 dispatch를 전달한 수 있어 deep update를 요구하는 컴포넌트의 경우 performance를 최적화할 수 있다.
What is deep update in React?
먼저, React에서 update란; useState() 를 사용하는 모든 리액트 컴포넌트에서 사용가능한 함수로, 컴포넌트 state가 변경되었을 때 리액트에게 이를 알림으로써 해당 state에 종속된 UI를 업데이트하기 위해 컴포넌트에게 리렌더링이 필요하다는 것을 전달한다.
따라서, deep update란 deep nested 객체 (many pieces of information과 fixed 스키마를 가진) 를 update 하는 것을 의미한다.
useCallback() 을 이용하는 이유는, callback 함수인 cb의 memoized 버전을 받기 위해서이다.
컴포넌트가 처음으로 렌더링될 때, 새로운 cb 가 생성되고 똑같은 cb 함수가 useCallback() 에 의해 반환된다.
위 예시에서 memoizedCb() 가 호출되면, original cb 콜백함수가 호출되어 결과적으로 original cb 함수를 호출한다.
컴포넌트가 리렌더링 (2번째) 되면, 새로운 cb 가 생성된다. 하지만 useCallback() 에 전달된 dependency가 변하지 않았기 때문에, 생성된 cb는 버려지고 memoizedCb는 첫번째 렌더링 때 생성된 cb를 유지한다. 결국 이 때의 memoizedCb 호출은 이전과 같이 기존의 (old) cb 를 호출하여 original cb 함수를 호출하게 된다.
dependency가 변하지 않는 이상, 모든 렌더링은 위와 같은 과정을 반복하여 사용하지 않는 콜백함수를 생성하고 기존의, memoized 콜백을 호출하여 결국 original cb 함수를 호출한다.
dependency 가 변한 경우, memoizedCb 도 변한다.
이전의 memoizedCb 가 첫번째 cb를 호출하는 과정과 비슷하게, 이 때의 memoizedCb 또한 dependency 가 변한 시점부터 첫번째 cb 를 호출한다.
callback
특정 함수 (콜백함수) 를 다른 함수에 전달하고 이를 호출하는 특정 이벤트 (e.g. a query finished or an error occurs) 가 발생하면 콜백함수를 발생시키는 것
여러 조각의 코드의 소통을 정의하여, 콜백함수를 호출하는 모듈이 결과를 사용하는 것이 아니라 이벤트가 발생했을 때 호출을 받는 함수에 콜백함수를 전달한다.
여기서 중요한 점은, one piece of code가 콜백함수를 호출하면 다른 (조각의) 코드가 이 호출을 받는다는 것이다. 여기서 어떠한 호출 코드도 잃으면 (생략되면) 안 되기 때문에, 콜백함수를 memoizing하여 모듈 간 소통 line을 끊는 방법이 탄생한 것이다.
결과적으로 useCallback() 이 하는 것은 똑같은 callback 의 횟수를 제한하는 것이다. 결과값들은 캐시에서 반환되지 않고, dependency 가 변한 시점으로부터 첫번째의 cb 와 같은 memoizedCb 를 반환한다.
dependency 가 변할 때에만 memoized 값을 다시 계산한다. 이를 통해 비싼 계산 과정이 매 렌더링마다 발생하는 것을 방지한다.
2번째 인자 (dependencies 배열 로서) 에 빈 배열 ( [] ) 이 전달되면, 매 렌더링 마다 새로운 값이 계산된다.
useMemo() 에 전달된 create 함수 는 렌더링 동안 실행된다.
보통 side effect는 useMemo 가 아니라 useEffect 에서 발생한다. 따라서 렌더링 동안은 다른 행동을 하지 않아야 한다. (당연함 -.-)
❗️useMemo() 는 성능 최적화에만 사용하고, 큰 의미적 기능 (semantic guarantee) 을 담지 않아야 한다. 따라서 useMemo() 를 사용하지 않고도 동작할 수 있도록 코드를 작성하고, 성능 최적화를 위해 useMemo() 를 추가하도록 한다.
앞선 useEffect, useCallback 과 마찬가지로, depedencies 배열 은 콜백함수에 인자로 전달되지 않는다.
useRef
JavaScript를 이용해 특정 DOM 을 선택해야 하는 경우 getElementById, querySelector 와 같은 DOM Selector 함수 를 사용한다.
리액트를 사용할 때도 특정 DOM 을 선택해야 하는 상황이 발생하는데, 이 때 ref 를 사용한다.
함수형 컴포넌트에서 ref 를 사용 할 때에는 useRef 라는 Hook 함수 를 사용한다.
클래스형 컴포넌트에서는 콜백 함수를 사용하거나 React.createRef 라는 함수를 사용한다.
1
const refContainer = useRef(initialValue);
useRef() : (생성된 ref 객체의).current 프로퍼티 (속성) 가 전달된 인자 (e.g. initialValue) 로 초기화된, 변경 가능한 ref 객체를 반환한다.
변경가능한 값을 넣을 수 있는 .current 프로퍼티를 갖고 있는 “박스” 와 같다.
해당 객체는 컴포넌트의 full lifetime 동안 지속된다.
useRef() 는 DOM에 접근하는 방법으로 많이 사용된다.
useRef() 를 사용하여
Ref 객체를 만들고,
이 객체를 우리가 선택하고 싶은 DOM (노드) 에 ref 값으로 설정한다.
그러면, Ref 객체의 .current 값은 우리가 원하는 DOM 을 가르키게 된다.
1
<div ref={refContainer} />
refContainer 의 .current 프로퍼티를 해당 ref 객체 ( refContainer ) 를 ref 로 설정한 DOM 노드로 설정하고, 노드가 변경될 때마다 그에 맞게 .current 프로퍼티를 조정한다.
하지만 useRef() 는 .current 에 변경 사항이 존재하더라도 리렌더링 하지 않기 때문에, 이에 대해 notify 하지 않는다. 만약 리액트가 DOM 노드에 ref 객체를 연결하거나 제거할 때 notify 하고 싶다면, callback ref 를 이용해야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
functionTextInput_With_FocusButton() { // inputEl : ref 객체 (null로 초기화됨) const inputEl = useRef(null); // button을 클릭하면 inputEl.current 인 input node에 focus() 를 적용한다. const onButtonClick = () => { // `current` points to the mounted text input element inputEl.current.focus(); }; return ( <> // inputEl ref 객체를 ref로 설정하여 inputEl.current 프로퍼티를 input node 로 설정한다. <input ref={inputEl} type="text" /> // onClick 시 onButtonClick 함수를 실행하고 // 👉 goto onButtonClick comment <button onClick={onButtonClick}>Focus the input</button> </> ); }
HTMLElement.focus()
특정 element 에 (focus를 맞출 수 있을 때) focus를 맞추는 함수로, 해당 element는 키보드 등의 이벤트를 받는 default element로 정의된다.
useRef() 는 ref 속성 외에도, 클래스가 instance field (인스턴스 변수) 를 가지고 있는 것처럼 ref 객체에 변경 가능한 어떠한 값도 담을 수 있다는 장점이 있다.
이는 useRef() 가 plain JavaScript 객체 를 생성하기 때문인데, 직접 {current: ...} 로 객체를 생성하는 것과 유일한 차이점은 useRef() 는 매 렌더링마다 똑같은 ref 객체를 제공 (반환) 한다는 것이다.
class field (클래스 변수) vs. instance field (인스턴스 변수)
클래스 변수 : 여러 인스턴스 (서로 다른 객체) 간에 공유해야 하는 값을 바인딩
인스턴스 변수 : 각 인스턴스 (객체) 마다 가지고 있는 고유한 값
파이썬은 인스턴스를 통해 접근한 이름 (변수) 이 인스턴스의 name_space (name들을 정의한 공간) 에 없을 경우, 그 다음으로 클래스의 name_space에서 찾아본다.
참고로 클래스 변수에 접근할 때에는 클래스 이름을 사용하여 바로 변수 (값) 에 접근할 수 있다.
useImperativeHandle
❗️ref를 사용하는 imperative code 는 되도록이면 피하는 것이 좋다. ):-<
ref를 사용할 때 부모 컴포넌트에 노출되는 인스턴스 값 (변수) 을 커스터마이징 한다.
즉, forwarding된 ref를 replace할 수 있는 기능을 제공한다.
1
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle() : ref, 콜백함수 (createHandle), dependnecies 배열 을 전달
createHandle() : 현재 컴포넌트의 값을 부모 컴포넌트가 접근할 수 있도록 하는 콜백함수
<FancyInput ref={inputRef} /> 를 수행 (렌더링) 하는 부모 컴포넌트는 inputRef.current.focus() 를 호출할 수 있다.
useLayoutEffect
useEffect 와 동일한 기능을 DOM 변경 시 동시에 (synchronously) 실행하는 함수로, DOM 에서 레이아웃을 읽음과 동시에 리렌더링을 해야할 때 사용한다.
useLayoutEffect 내부에 정의된 예정된 업데이트 또한 브라우저가 레이아웃을 그리기 이전에, 읽음과 동시에 발생한다.
화면 업데이트를 차단하지 않아도 되는 경우에는 useEffect 를 사용하는 것이 좋다.
클래스 컴포넌트에서 코드를 옮길 때 useLayoutEffect() 는 componentDidMount 나 componentDidUpdate 와 같은 단계에서 발생한다. 하지만, 먼저 useEffect 를 시도해보고 에러가 발생할 때 useLayoutEffect 를 사용하는 것이 좋다.
SSR (서버 사이드 렌더링) 의 경우, useLayoutEffect 나 useEffect 는 JavaScript 가 모두 다운되기 전까지는 실행되지 않는다. SSR 컴포넌트가 useLayoutEffect 를 포함한 경우,
첫 렌더링 시 해당 컴포넌트가 필요하지 않다면 useEffect 로 로직을 옮기고,
useLayoutEffect 가 실행되기 전까지 HTML이 망가져 보인다면 클라이언트 렌더링이 완료될 때까지 해당 컴포넌트를 보여주는 것을 딜레이시킨다.
서버 렌더링 HTML에서 레이아웃 effect를 필요로 하는 컴포넌트를 제외하기 위해서,
showCild && <Child /> 를 이용하여 해당 컴포넌트를 조건적으로 렌더링을 하고,
useEffect(() => { setShowChild(true); }, []) 를 이용하여 보이는 것을 미룬다 (defer).
이를 통해 HTML이 hydration (useLayoutEffect, useEffect 실행) 이전에 망가져 보이는 것을 방지한다.
useDebugValue
1
useDebugValue(value)
React DevTools 에서 커스텀 Hooks의 label (value) 을 보여준다.
// Show a label in DevTools next to this Hook // e.g. "FriendStatus: Online" useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline; }
모든 커스텀 Hook에 debug value를 추가하는 것은 좋지 않다.
shared libraries에 속하는 커스텀 Hooks에 유용하게 사용될 수 있다.
Defer Formatting Debug Values
디버그 값 포맷팅 지연하기
디스플레이 (되는) 값을 포매팅하는 것은 고비용 연산일 수 있고, Hook이 감지되지 않은 경우 사실상 포매팅은 불필요하다. 따라서, useDebugValue 의 (optional한) 2번째 인자로 포매팅 함수 를 전달하여 Hook이 감지되었을 때만 해당 함수를 호출하여 포매팅할 수 있다.
1
useDebugValue(value, value => value.formatting_function());
useDebugValue() : value 와 optional formatting 함수 를 전달
optional formatting function : debug value 를 인자로 전달하고 formatted display value 를 반환한다.
1
useDebugValue(date, date => date.toDateString());
2번째 인자로 fomatting function 을 전달하여 Date value 를 반환하는 커스텀 Hook이 toDateString() 을 불필요하게 호출하는 것을 방지한다.
기존에 작성한 HTML 코드와 동일할 수도 있지만, 대게 브라우저에 파싱됨에 따라 변경사항이 적용되어 브라우저에 보여지는 형태로 변형되어 보여짐.
웹 페이지는 일종의 문서(document)
The backbone of an HTML document is tags
웹 페이지의 객체 지향 표현 (독립적인 객체) 이며, 자바스크립트와 같은 스크립팅 언어를 이용해 DOM 을 수정
HTML, XML 문서의 프로그래밍 interface 로, 프로그래밍 언어가 DOM 구조에 접근할 수 있는 방법을 제공한다.
DOM 은 문서를 표현하고, 저장하고, 조작하는 (문서 구조, 스타일, 내용 등을 변경) 방법을 제공한다.
웹 페이지를 스크립트 또는 프로그래밍 언어들에서 사용될 수 있게 연결시켜주는 역할
문서의 구조화된 표현(structured representation)
node, property, method 를 갖고 있는 object들로 구조화된 문서를 표현
document object 는 document 자체
e.g. table object 는 HTML table 에 접근하기 위한 HTMLTableElement DOM interface를 구현하는 것이다.
동일한 문서를 사용하여 다른 형태로 나타날 수 있다.
웹 브라우저를 통해 그 내용이 파싱(해석)되어 웹 브라우저 화면에 나타나거나,
기존 HTML 소스 자체로 나타나기도 한다.
Everything in HTML, even comments, becomes a part of the DOM.
DOM과 기존 HTML 코드가 달라지는 경우
브라우저가 HTML을 수정하는 경우 : 브라우저 파싱 시 자동 교정 (Autocorrection)
Spaces and newlines before <head> are ignored for historical reasons.
If we put something after </body>, then that is automatically moved inside the body, at the end, as the HTML spec requires that all content must be inside <body>. So there can’t be any spaces after </body>.
if there are spaces (just like any character) in the document, then they become text nodes in the DOM
Tables always have <tbody>
JavaScript로 DOM을 조작하는 경우 : 조작 (변경) 사항이 반영되어 DOM 출력
Ajax : 컨텐츠를 가져와 (c.f. fetch) 페이지에 추가하는 경우
Templating : server의 내용을 테스트하기 위해 임의의 client-side template을 적용
e.g. handlebars.js
DOM과 JavaScript
JavaScript : 브라우저가 읽고 작업을 할 수 있는 언어, 브라우저에 기록되는 것들을 조작할 수 있는 문법/ 언어
DOM API 를 이용해서 작업
DOM : 작업이 이루어지는 장소, 브라우저에 의해 기록되는 모든 것
DOM 이 없다면 자바스크립트 언어는 웹 페이지 또는 XML 페이지의 요소들과 관련된 모델이나 개념들에 대한 정보를 갖지 (불러오지) 못하게 된다.
문서의 모든 element는 DOM의 한 부분
페이지 콘텐츠 (element) 는 DOM 에 저장되고 자바스크립트를 통해 접근하거나 조작
1
API (web or XML page) = DOM + JS (scripting language)
DOM 의 구현은 어떠한 언어에서도 가능
e.g. python 에서도 구현 가능
DOM에 접근하는 방법
문서가 로드될 때는 모든 DOM을 사용할 수 있게 되는 때이다.
스크립트를 작성할 때
직접적으로 문서 자체를 조작하거나,
간접적인 조작을 위해 문서의 children 을 얻는, document 또는 window element 를 이용하는 API 를 사용할 수 있다.
1
<bodyonload="window.alert('welcome to my home page!');">
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
<html> <head> <script> // run this function when the document is loaded window.onload = function() {
// create a couple of elements in an otherwise empty HTML page var heading = document.createElement("h1"); var heading_text = document.createTextNode("Big Head!"); heading.appendChild(heading_text); document.body.appendChild(heading); } </script> </head> <body> </body> </html>
Data Type
Tags are element nodes (or just elements) and form the tree structure.
DOM node는 element 로,
document – the “entry point” into DOM.
element nodes – HTML-tags, the tree building blocks.
text nodes – contain text.
comments – sometimes we can put information there, it won’t be shown, but JS can read it from the DOM.
노드의 array는 nodeList (또는 element),
attribute 노드는 attribute 로
document
document type 의 object 를 리턴할 때, 이 object 는 root document object 자체이다. 예를 들어 element의 ownerDocument property 는 그것이 속해 있는 document 를 return 한다.
element
element 는 DOM API에 의해 리턴된 element 또는 element type 의 DOM node 를 의미한다. 예를 들어 document.createElement() 는 node를 참조하는 object 를 리턴하며, DOM 안에서 생성되는 element를 리턴한다고 말할 수 있다.
nodeList
nodeList 는 element들의 배열이다. document.getElementsByTagName() 와 같은 method에 의해 리턴된 nodeList의 Item들은 index 를 통해 접근 가능하며, 다음과 같이 두 가지 방식이 있다 (동일한 방식임) : - list.item(1) - list[1] item() 은 nodeList object 의 단일 method 이다. 두번째 방식은 list 에서 두번째 item 을 fetch 하는 전형적인 array syntax 이다.
attribute
API에 의해 attribute가 리턴되는 것은 attribute에 대한 특별한 인터페이스를 노출하는 object reference 가 표현됨을 의미한다. attribute는 DOM 에서 element와 같은 node이다.
namedNodeMap
namedNodeMap 는 array 와 유사한 list로, item은 name 또는 index 에 의해 접근 가능하다. 리스트는 특별한 정렬이 적용되지 않았기 enumeration (열거, 목록화) 할 때 index 를 주로 사용한다. namedNodeMap 는 이를 위해 item() method 가 있으며, namedNodeMap 에 item 을 추가하거나 삭제할 수 있다.
DOM interface
object 와 DOM 에서 조작 가능 한 것
API 를 이용해 접근 및 조작
Interface와 Object
Every tree node is an object.
많은 object가 여러 개의 다른 interface와 연관되어 있다.
HTML element 는 DOM 이 연관되어 있는 한 node 트리에서 하나의 node를 담당한다.
Node : 기본적인 Element
node 트리는 웹 페이지 또는 XML 페이지를 위한 object model을 구성한다.
1
table object 는 createCaption, insertRow method 들이 포함된 HTMLTableElement 을 구현한 것이다
table Element 는 Node interface 를 구현하고 있다.
table object 는 HTML Element (object model) 이기도 하기 때문에, table 은 Element interface도 구현한다.
table object 를 참조하게 되면, 기본적으로 이 3 가지 interface를 사용할 수 있다.
1 2 3 4 5 6 7 8 9
var table = document.getElementById("table"); // table === node/ element var tableAttrs = table.attributes; // Node/Element interface for (var i = 0; i < tableAttrs.length; i++) { // HTMLTableElement interface: border attribute if(tableAttrs[i].nodeName.toLowerCase() == "border") table.border = "1"; } // HTMLTableElement interface: summary attribute table.summary = "note: increased border";
핵심 Interface
Document 와 window object 는 DOM 프로그래밍에서 가장 자주 사용하는 object
window object 는 브라우저와 같다.
document object 는 root document 자체이다.
DOM 을 사용하는 공통적인 API
document.getElementById(id)
document.getElementsByTagName(name)
document.createElement(name)
parentNode.appendChild(node)
element.innerHTML
element.style.left
element.setAttribute()
element.getAttribute()
element.addEventListener()
window.content
window.onload
window.dump
window.scrollTo
Interaction with console
select the particular element (e.g. <li>) and press Esc opens the console
or just run inspect(node)
the last selected element is available as $0, the previously selected is $1 etc.
in console tab, run the command on the element
1 2
// changes the background color of the inspected <li> element $0.style.background = 'red'