리액트에서 외부 링크를 열어야 하는 경우
당연히 a 테그 만들어서 클릭했을때 열리게 하면 되는거 아니냐~ 라고 생각 하겠지만,
서비스 만들다 보면 엄청 많은 경우의 수가 발생한다.
일단 링크의 수가 수십 개가 넘어가면 관리가 안된다.
링크가 자주는 아니더라도 가끔 바뀌더라도 찾아서 변경하는게 여간 귀찮은 일이 아니다.
만약에 다른 서비스 안에 들어가는 웹뷰로 서비스를 한다면?
브라우저 창을 열 수 없다.
Iframe 등을 이용해서 우리 서비스 내부에서 링크 내용을 띄워야 한다.
공개망 / 사설망 둘다 지원해야하는 서비스라면?
사설망에서는 브라우저를 열 수 없다.
미리 외부 링크의 문서 내용을 HTML 로 변환해두던가, 내부 서버를 통해 다운로드 받을 수 있게끔 해줘야 한다.
해결 방법
일단 흩어진 링크들을 상수 파일로 가져온다. (예시는 2개 뿐이지만, 실제론 수십 개는 있다고 가정)
// constants/link.ts
export const LINKS = {
가이드: 'https://...',
오류신고: 'https://forms/...'
}
export const PRIVATE_LINKS = {
가이드: `${document.location.origin}/guide/guide.html`,
오류신고: 'https://forms/...'
}
저 LINKS 를 이용해서 LinkType 을 만들어준다.
export type LinkType = keyof typeof LINKS | undefined
외부 링크를 사용하는 컴포넌트 안에
useOuterLink hook 을 import 해주고 링크 클릭시 openWindow('링크명') 함수를 호출하게 한다.
사설망에서는 PRIVATE_LINKS 사용하고, 그 외에는 그냥 LINKS 상수를 사용하게 제어해줄 수 있다.
// useOuterLink.ts
import { useCallback } from 'react'
import { useTypedDispatch } from './useStore'
import { ENV, isPrivateNetwork } from '@src/constants/env'
import { LINKS, PRIVATE_LINKS } from '@src/constants/link'
import { LinkType, setLinkUrl } from '@src/modules/linkStore'
export const useOuterLink = () => {
const dispatch = useTypedDispatch()
const openWindow = useCallback(
(url: LinkType) => {
const linkUrl = url ? (url in LINKS ? LINKS[url] : '') : ''
if (isPrivateNetwork) {
const linkUrl = url ? url in PRIVATE_LINKS ? PRIVATE_LINKS[url] : '': ''
const windowFeatures = 'noopener, noreferrer'
if (linkUrl) {
const newWindow = window.open(linkUrl, '_blank', windowFeatures)
if (newWindow) {
newWindow.focus()
}
}
return
}
if (ENV === '특정 환경') {
dispatch(setLinkUrl(url))
} else {
const windowFeatures = 'noopener, noreferrer'
if (linkUrl) {
const newWindow = window.open(linkUrl, '_blank', windowFeatures)
if (newWindow) {
newWindow.focus()
}
}
}
},
[dispatch]
)
return { openWindow }
}
참고로 저 dispatch(setLinkUrl(url)) 코드는 Redux Store 내부에 외부 링크 url 을 저장한 것이다.
Redux 를 굳이 안써도 된다. 대강 이런 식으로 사용한다고 참고하면 된다.
// modules/linkStore.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { LINKS } from '@src/constants/link'
export type LinkType = keyof typeof LINKS | undefined
interface LinkState {
linkUrl: LinkType
}
const initialState: LinkState = {
linkUrl: undefined
}
const linkSlice = createSlice({
name: 'link',
initialState,
reducers: {
setLinkUrl(state, action: PayloadAction<LinkType>) {
state.linkUrl = action.payload
}
}
})
export const { setLinkUrl } = linkSlice.actions
export default linkSlice.reducer
그리고 아래와 같이 OuterLinkWindow 컴포넌트를 App.js 같은 신경쓰지 않아도 되는 적당한 위치에 집어넣으면 된다.
이 컴포넌트는 Store에 저장해둔 url 을 가져와서 Iframe 으로 렌더링하는 역할을 한다.
- iframe 의 스타일은 알아서 추가해주면 된다.
- FullSizeWindow 컴포넌트는 닫기 버튼을 추가해 둔 것이다.
// OuterLinkWindow.tsx
import { useTypedSelector } from '@src/lib/hooks/useStore'
import { useOuterLink } from '@src/lib/hooks/useOuterLink'
import { LINKS } from '@src/constants/link'
import { FullSizeWindow } from './FullSizeWindow'
export const OuterLinkWindow = () => {
const url = useTypedSelector(state => state.linkStore.linkUrl)
const { openWindow } = useOuterLink()
const linkUrl = url ? (url in LINKS ? LINKS[url] : '') : ''
return (
<FullSizeWindow
isRender={!!linkUrl}
handleClose={() => openWindow(undefined)}
>
<iframe
title="outer"
frameBorder={'none'} // border 제거
src={linkUrl}
/>
</FullSizeWindow>
)
}
// App.tsx
return (
<>
<OuterLinkWindow />
<Routes>
...
</Routes>
</>
)
그렇게 복잡하지 않게 해결되었다.
동료 개발자들은, 모든 링크가 constants/link.ts 에 정리되어 있고
외부 링크를 추가할 때 a 테그 대신에 hook을 사용한다는 것만 인지하면 된다.
'프론트엔드 개발 > React.js' 카테고리의 다른 글
Vite 아직 사용하지 마세요!!! (1) | 2023.01.28 |
---|---|
CRA를 Vite로 마이그레이션 하기 (0) | 2023.01.15 |
리액트에서 마우스가 브라우저 창 밖에 있는지 감지하기 (0) | 2023.01.15 |
리액트 장치 유형 감지하기 (0) | 2023.01.15 |
React Query onError 가 동작하지 않는 이슈 (0) | 2022.12.21 |