useState는 initial value를 구독하지 않는다.
function Textfield({ initialText }: { initialText: string}) {
const [text, setText] = useState(initialText)
console.log("(re)rendered with props: ", {initialText})
return <input type="text" value={text} onChange={({target: { value }})=> setText(value)}/>
}
function App() {
const [text, setText] = useState("Hello")
const handleClick = () => {
console.log("Button is clicked")
setText("Bye")
}
return (
<div className="Content">
<button onClick={handleClick}>Hello</button>
<Textfield initialText={text} />
</div>
);
}
위와 같은 리액트 앱이 있습니다. 위에서 Hello 버튼을 클릭하면 어떻게 될까요?
아마도 대부분 Textfield에 전달된 프로퍼티가 변경되었으니 재렌더링이 될 것입니다.
그러면서 text 상태도 변경된 "Bye" 값으로 새로 초기화 되겠죠.
하지만 이것은 틀렸습니다.
실제로 실행해서 확인해보면 "Bye"라는 값으로 프로퍼티가 새로 전달되어 재렌더링 되었지만 text 상태는 그대로인 것을 볼 수 있습니다.
리액트 공식 문서에서 useState의 Parameters에 대한 설명을 읽어보면 다음과 같은 언급이 있습니다.
initialState: The value you want the state to be initially. It can be a value of any type, but there is a special behavior for functions. This argument is ignored after the initial render.
- react.dev
즉, useState의 인자로 넘긴 초기값은 첫 렌더링 이후 부터는 무시됩니다.
이는 useState의 인자로 초기화 함수를 넘겨 확인할 수 있습니다.
function Textfield({ initialText }: { initialText: string}) {
const [text, setText] = useState(() => {
console.log("Textfield is initialized")
return initialText
})
console.log("(re)rendered with props: ", {initialText})
return <input type="text" value={text} onChange={({target: { value }})=> setText(value)}/>
}
...
Textfield 컴포넌트를 위와 같이 변경하고 다시 실행해보면, 버튼을 누른 뒤 재렌더링이 되더라도 초기화 함수는 다시 호출되지 않는 것을 확인할 수 있습니다.
여기서 상태가 초기화되는 첫 렌더링 시점은 컴포넌트가 DOM 트리에 마운트 되는 시점입니다.
만약, DOM 트리에서 언마운트 되었다가 다시 DOM 트리에 마운트 된다면 상태는 새로 초기화 됩니다.
다음 예시를 살펴봅시다.
function Textfield({ initialText }: { initialText: string}) {
const [text, setText] = useState(() => {
console.log("Textfield is initialized")
return initialText
})
console.log("(re)rendered with props: ", {initialText})
return <input type="text" value={text} onChange={({target: { value }})=> setText(value)}/>
}
function App() {
const [text, setText] = useState("Hello")
const [show, setShow] = useState(true)
const sayBye = () => {
console.log("Hello button is clicked")
setText("Bye")
}
const toggleShow = () => {
console.log("Toggle button is clicked")
setShow((prev) => !prev)
}
return (
<div className="Content">
<button onClick={sayBye}>Hello</button>
<button onClick={toggleShow}>Toggle</button>
{show && <Textfield initialText={text} />}
</div>
);
}
Toggle 버튼을 누르면 Textfield의 렌더링 여부를 변경할 수 있습니다.
Toggle 버튼을 눌러 Textfield를 언마운트하고, Hello 버튼을 누르고 다시 Toggle 버튼을 눌러 Textfield를 마운트하면 어떻게 될까요?
예상했다시피, 다시 상태가 초기화되는 것을 볼 수 있습니다.
DOM 트리에 마운트 되는 것이 최초 렌더링을 의미합니다. (비록 언마운트 되었다가 다시 마운트 되는 것이더라도)
그렇다면 프로퍼티가 변경되면 그에 따라 상태도 따라 변경하고 싶다면 어떻게 해야 할까요?
매번 해당 컴포넌트를 재마운트를 해야 하는걸까요?
다행히 간단한 방법이 있습니다. useEffect를 사용하면 됩니다.
function Textfield({ initialText }: { initialText: string}) {
const [text, setText] = useState(() => {
console.log("Textfield is initialized")
return initialText
})
useEffect(() => {
setText(initialText)
}, [initialText])
console.log("(re)rendered with props: ", {initialText})
return <input type="text" value={text} onChange={({target: { value }})=> setText(value)}/>
}
...
useEffect의 의존성 배열에 전달받은 프로퍼티를 추가합니다.
그러면 프로퍼티의 값이 바뀔 때마다 상태는 그 시점의 프로퍼티 값으로 새로 변경됩니다.
Hello 버튼을 누르면 먼저 Textfield에 프로퍼티가 "Bye"로 새로 전달되고, 재렌더링이 됩니다.
그리고 useEffect에서 변경된 프로퍼티를 상태를 변경하여 새로 재렌더링이 됩니다.
다른 방법으로 리액트의 특성을 이용하는 방법이 있습니다.
컴포넌트의 key 속성을 변경하면 리액트는 해당 컴포넌트를 다시 생성합니다. (재렌더링과 다릅니다.)
function Textfield({ initialText }: { initialText: string}) {
const [text, setText] = useState(() => {
console.log("Textfield is initialized")
return initialText
})
console.log("(re)rendered with props: ", {initialText})
return <input type="text" value={text} onChange={({target: { value }})=> setText(value)}/>
}
function App() {
const [text, setText] = useState("Hello")
const [key, setKey] = useState(0)
const sayBye = () => {
console.log("Hello button is clicked")
setText("Bye")
setKey((prev) => prev + 1)
}
return (
<div className="Content">
<button onClick={sayBye}>Hello</button>
<Textfield initialText={text} key={key} />
</div>
);
}
위에서 확인할 수 있다시피 DOM 트리에서 언마운트 되었다가 다시 DOM 트리로 마운트되는 것이기 때문에, Textfield의 상태가 초기화됩니다.
하지만 이 방식은 재렌더링보다 비용이 비쌉니다.
특히 자식 컴포넌트가 많이 포함되어 있는 컴포넌트일수록 좋은 선택이 아닐 가능성이 큽니다.
따라서 상황에 따라 적절한 방법을 선택하여 상태를 프로퍼티와 동기화하면 되겠습니다.