인턴 업무 중 하나로, 여러 콘텐츠들의 list가 있고, 하나의 콘텐츠를 클릭하면 해당 콘텐츠의 상세 보기 뷰가 보이도록 하는 일을 맡았다. 99% 기능을 다 구현했는데, 하나의 문제 때문에 며칠을 고민했다.
발생한 문제는, 상세보기 페이지에서 뒤로 가기를 클릭하면 한번에 list로 돌아가지지 않고, 여러번을 눌러야 이동하는 문제였다.
개발자 도구에서 네트워크를 확인해보니, 상세 페이지에 들어가는 유튜브 동영상이 뒤로 가기 클릭 시 자꾸 reload 되는 게 문제인 것 같았다. 처음에 코드가 _get
메서드를 통해, 유튜브 동영상 아이디를 불러오게 되어있어서, 이게 자꾸 로드되어 그런가해서 useEffect
를 사용해, 초기 로드 시에만 호출되게 했다.
하지만, 문제는 여전히 그대로였다. 그렇게 고민에 고민을 하며 구글링하던 중, 큰 힌트를 얻었다.
Pressing the browser back button undoes my state instead of going back
유튜브 동영상을 넣을 때 사용한 iframe
에 unique한 key 값을 넣지 않아 react router에 혼란을 주게 되어 발생한 문제이다. 이 문제를 해결하기 위해선, 단순히 iframe
을 출력하는 컴포넌트를 호출할 때, unique한 key 값을 주면 된다.
const YoutubeEmbed = ({ embedId }) => (
<div className="video-responsive">
<iframe
width="853"
height="480"
title="Embedded youtube"
src={`https://www.youtube.com/embed/${embedId}`}
/>
</div>
);
export default function App() {
// . . .
return (
<div className="video">
<YoutubeEmbed key={currentVideo} embedId={currentVideo} />
</div>
);
}
iframe은 동적인 src
속성을 가지고 있다. 즉 앱의 다른 페이지로 이동할 때 콘텐츠가 변경된다. 하지만 우리가 보기에는 항상 동일한 iframe을 사용하는 데, 이 이유는 우리는 단순히 re-rendering만 진행하고 다른 URL을 가리키기 때문이다.
근본적으로 내가 겪은 문제가 발생하는 이유는,
"Iframe이 브라우저의 Back Navigation을 방해"하기 때문이다.
iframe이 포함되어있는 페이지에서, 뒤로 가기 버튼을 클릭하면, iframe 자체가 스스로 뒤로 이동하여 이전 페이지에 존재하던 콘텐츠(iframe의 콘텐츠)를 가리킨다. 하지만 iframe이 뒤로 이동한 반면, 애플리케이션은 뒤로 이동하지 않고, 현재 페이지에 머무른다. 즉, 페이지와 iframe이 동기화되어 움직이지 않게 되는 것이다. 여기서 한번 더 뒤로 가기 버튼을 누르면, 그제서야 올바른 페이지로 이동하게 된다.
내가 겪은 문제가 정확히 이런 상황이다. 원하는 페이지를 탐색하기 위해 항상 뒤로 가기 버튼을 두번 눌러야하는 것이다.
iframe을 재사용하고, 다른 콘텐츠를 가리키도록 src
속성을 변경할 때마다, 이 동작은 'content navigation'으로 처리되어 브라우저의 window.history
스택에 추가된다. 이러한 상태에서 뒤로 가기 버튼을 클릭하면, history 스택은 가장 위에 먼저 들어가 있던걸 pop하기 때문에, iframe이 스스로 navigate되는 것이다.
실제로 window.history
를 확인해보면, iframe이 포함된 페이지에서는 length
가 2씩 증가하는 것을 볼 수 있다.
프레임에 구애받지 않고 이를 해결할 수 있는 방법은, iframe을 재사용하는 대신, src
를 변경해야 하는 경우마다 iframe 파괴하고 recreate 하는 것이다. React에서 이를 가능하게 하는 방법은 key
를 사용하는 것이다.
React에서는 'reconciliation process'의 일부로 key prop을 사용하여, 두 렌더링 간에 요소가 논리적으로 동일한지 여부를 판단한다. 동일한 요소가 표시되지 않는 경우, re-rendering 대신 해당 요소를 Unmount한 뒤 다시 mount한다.
React에서는 코드에서 요소의 위치가 두 렌더링에서 변경되지 않은 경우, 이전 렌더링에서 본 것과 동일한 요소로 작업하고 있다고 가정한다. 현재의 상태가 손실되는 것을 원하지 않기 때문에 단순히 re-rendering을 수행하지, 대부분의 경우에는 기존의 mount를 해제하고, 다시 mount하지 않는다.
하지만 UI가 변경될 때마다, 구성 요소가 완전히 mount가 해제되길 원하는 상황도 존재하는데, 지금 우리가 이야기하고 있는 상황이 그 상황이다. 이 문제를 해결하기 위해선, iframe 전체를 다시 rendering하는 것이 아니라, 다시 mount해야 한다.
모든 렌더링에서 고유한 키를(콘텐츠 id나 url 등을 사용) 제공하면 구성 요소를 강제로 다시 mount할 수 있다. iframe을 다시 mount하면, iframe이 브라우저 history stack에 push되는 것을 막을 수 있다.
이렇게 하면 iframe이 포함된 페이지에서도 뒤로 가기가 정상적으로 실행된다!!
React, Iframes, and a Back-Navigation Bug
배열을 순회할 때 unique한 key값이 없으면 늘 VSC는 에러를 뱉어내기 때문에 별 생각 없이 추가하던 key값을 정확히 어떤한 경우에 사용해야 하는 지, 어떻게 동작하는 지에 대해 제대로 배울 수 있는 기회였다.
앞으로도 더 깊게 생각하고, 공부해야겠다는 생각이 많이 들었던 에러 해결이었다...