ios 환경에서 input, textarea를 터치하면 화면이 자동으로 확대되는 현상이 있다. 이를 방지하기 위한 세 가지 방법이 있다.
1. viewport 설정하기
다음과 같이 index.html의 viewport meta 태그에 maximum-scale=1.0, user-scalable=0을 추가한다.
// index.html
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
가장 간단한 방법이며 나도 이전까지 많이 사용하던 방법이다. 그런데 이 설정을 추가하게 되면 웹 접근성에 좋지 않다는 걸 알게 되었다.
팀원 분이 lighthouse에서 수행한 접근성 검사에서 이런 결과가 나왔다. user-scalable을 막고 maximum-scale을 줄이는 게 사용자의 화면 확대를 막음으로써 접근성에 좋지 않다는 것이다. 하지만 저걸 설정한다고 해서 실제로 모바일 브라우저 환경에서 화면 확대가 되지 않는 것은 아니다. (정확한지는 알 수 없으나 저 설정이 있어도 화면 확대가 가능하도록 브라우저 환경이 변화하였다는 말을 보았다.)
접근성이 낮아지는 걸 감안하고 저걸 사용하는 사람들도 꽤 있는 것 같았다. 하지만 우리 팀은 실서비스 운영을 목표로 하여 웹 접근성을 중요하게 생각하고 있었기 때문에 저 방법이 아닌 다른 방법을 찾아보기로 했다.
2. input, textarea 폰트 사이즈 조절
ios 환경에서는 input, textarea 폰트 사이즈가 16px 이상이면 클릭해도 화면 확대가 되지 않는다! 어찌 보면 접근성을 해치지 않으면서 가장 간단하게 자동 화면 확대를 막을 수 있는 방법이다. 그러나 우리 팀은 16px이 좀 크다는 의견이 있었기에 이 방법도 넘겼다.
3. input, textarea 폰트 사이즈 조절 + scale 조절
이 방법은 위의 두 번째 방법을 활용한 방법이다. 우리가 선택한 방법이기도 하다. 폰트 사이즈를 16px로 설정하여 자동 화면 확대를 막으면서, input과 textarea의 크기를 scale로 줄여 실제로 눈에 보이는 폰트 사이즈를 16px 미만의 원하는 사이즈로 만드는 방법이다!
간단한 것 같지만 은근히... 번거롭다.
scale로 크기를 줄이면 폰트 사이즈뿐만 아니라 input, textarea의 width, height, padding, border-radius 등등 모든 속성이 함께 줄어들기 때문에 기존에 적용하고 있던 스타일 속성들이 있다면 모두 줄어드는 비율만큼 더 크게 설정해야 한다.
우리가 원하는 폰트 사이즈는 14px이었다.
transform: scale(0.875)를 적용하여 폰트 사이즈가 14px로 보이게 만들었다.
width, height, padding, border-radius 값도 줄어들 것을 고려하여 조금 더 크게 설정했다.
transform-origin으로 변환 중심점을 설정해 주어야 scale을 했을 때 상단과 좌측에 여백이 생기지 않는다.
--scale: 1.1429;
width: calc(100% * var(--scale));
height: calc(40px * var(--scale));
font-size: 16px;
transform: scale(0.875);
transform-origin: left top;
padding: ${hasIcon ? `0 1rem 0 calc(1.16rem + 20px)` : `0 1.16rem`};
scale과 스타일 속성을 모두 수정하여 기존 스타일과 유사하게 만들었다면 마지막으로 남은 것은 scale로 크기를 줄임으로써 발생한 하단 여백을 없애는 것이다! width의 경우 여백이 없었지만 height는 줄어드는 만큼 여백이 남았다. 이를 negative margin으로 제거해 주었다.
margin-bottom: calc(40px - 40px * var(--scale));
내가 구현한 input의 전체 코드는 다음과 같다.
export const TextInputBox = styled.input<TextInputBoxProps>`
${({ warn = false, hasIcon = false, disabled = false, theme }) => css`
${theme.typo["body-2-r"]};
--scale: 1.1429;
width: calc(100% * var(--scale));
height: calc(40px * var(--scale));
font-size: 16px;
transform: scale(0.875);
transform-origin: left top;
padding: ${hasIcon ? `0 1rem 0 calc(1.16rem + 20px)` : `0 1.16rem`};
margin-bottom: calc(40px - 40px * var(--scale));
border-radius: 7px;
display: flex;
border: 1px solid
${warn ? theme.colors["warn"]["40"] : theme.colors["neutral"]["30"]};
justify-content: space-between;
align-items: center;
flex-shrink: 0;
background-color: ${!disabled ? "white" : theme.colors["neutral"]["20"]};
color: ${!disabled
? theme.colors["neutral"]["90"]
: theme.colors["neutral"]["50"]};
transition: all 0.2s;
&::placeholder {
color: ${theme.colors["neutral"]["50"]};
}
&:hover {
${!warn &&
!disabled &&
`border: 1px solid ${theme.colors["primary"]["60"]};`}
}
// pressed
&:focus {
outline: none;
${!warn &&
!disabled &&
`border: 1px solid ${theme.colors["primary"]["60"]};`}
${!warn &&
!disabled &&
`box-shadow: 0px 0px 2px 0px rgba(17, 124, 255, 0.8);`}
}
`}
`;
input의 경우 height를 고정 값으로 설정하고 있어서 비교적 어렵지 않게 구현할 수 있었다. 하지만 textarea는 height가 고정 사이즈가 아니었기 때문에 좀 더 추가적인 과정이 필요했다.
textarea의 경우 height를 기본 auto로 설정하고, 필요시 다음과 같이 각 상황에 맞게 overriding 하여 사용하고 있었다.
import Textarea from "@/components/TextArea";
export const FormTextarea = styled(Textarea)`
width: calc(100% - 81px);
& > textarea {
height: 233px;
border-radius: 12px;
}
`;
input과 마찬가지로 scale을 설정하고, 스타일 속성들도 수정하였다. 문제는 scale로 인해 생긴 하단 여백을 없애기 위한 negative margin이었다. textarea의 높이가 다양하게 설정될 수 있다 보니 scale로 인해 생기는 여백의 크기도 다양했다. width가 아닌 height였기에 % 단위로 margin을 설정할 수도 없었고 상수로 설정할 수도 없었다. 더군다나 우리의 경우 textarea의 error message를 textarea 요소 바로 밑에 함께 두고 있었기 때문에 반드시 이 거슬리는 여백을 없애야 했다.
그래서 useRef를 통해 textarea의 높이와 scale로 인해 생긴 여백의 크기를 구한 뒤, textarea 컴포넌트의 props로 negative margin 값을 넘겨주기로 했다. negative margin 값은 여백의 크기에 -1을 곱하여 구했다.
// Textarea/index.tsx
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const [negativeMargin, setNegativeMargin] = useState(0);
useEffect(() => {
if (textareaRef.current) {
const offsetHeight = textareaRef.current.offsetHeight; // 원본 textarea height
const height = offsetHeight * 0.875; // scale 적용이 된 height
setNegativeMargin(-(offsetHeight - height)); // negative margin
}
}, []);
return (
<TextareaBox
negativeMargin={negativeMargin}
ref={textareaRef}
warn={warn}
{...rest}
/>
);
// Textarea/style.ts
export const TextareaBox = styled.textarea<TextareaBoxProps>`
${({ warn = false, theme, negativeMargin }) => css`
margin-bottom: ${negativeMargin}px;
...
`}
`;
구현한 textarea의 전체 코드는 다음과 같다.
// Textarea/index.tsx
import { ComponentProps, useEffect, useRef, useState } from "react";
import { Message, TextareaBox, TextareaContainer } from "./style";
export interface TextareaProps extends ComponentProps<"textarea"> {
warn?: boolean;
message?: string;
}
export interface TextareaBoxProps extends ComponentProps<"textarea"> {
warn?: boolean;
negativeMargin: number; // scale 후 여백 제거를 위한 negative margin
}
export default function Textarea({
warn = false,
message = "",
style,
className = "",
...rest
}: TextareaProps) {
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const [negativeMargin, setNegativeMargin] = useState(0);
useEffect(() => {
if (textareaRef.current) {
const offsetHeight = textareaRef.current.offsetHeight; // 원본 textarea height
const height = offsetHeight * 0.875; // scale 적용이 된 height
setNegativeMargin(-(offsetHeight - height));
}
}, []);
return (
<TextareaContainer style={style} className={className}>
<TextareaBox
negativeMargin={negativeMargin}
ref={textareaRef}
warn={warn}
{...rest}
/>
{warn && message !== "" && <Message>{message}</Message>}
</TextareaContainer>
);
}
// Textarea/style.ts
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { TextareaBoxProps } from ".";
export const TextareaBox = styled.textarea<TextareaBoxProps>`
${({ warn = false, theme, negativeMargin }) => css`
${theme.typo["body-2-r"]};
--scale: 1.1429;
width: calc(100% * var(--scale));
font-size: 16px;
transform: scale(0.875);
transform-origin: left top;
padding: 0.6rem 1.16rem;
margin-bottom: ${negativeMargin}px;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
background-color: white;
color: ${theme.colors["neutral"]["90"]};
border-radius: 7px;
border: 1px solid
${warn ? theme.colors["warn"]["40"] : theme.colors["neutral"]["30"]};
transition: border 0.2s;
resize: none;
&::placeholder {
color: ${theme.colors["neutral"]["50"]};
}
&:hover {
${!warn && `border: 1px solid ${theme.colors["primary"]["60"]};`}
}
// pressed
&:focus {
color: ${theme.colors["neutral"]["90"]};
outline: none;
${!warn && `border: 1px solid ${theme.colors["primary"]["60"]};`}
${!warn && `box-shadow: 0px 0px 2px 0px rgba(17, 124, 255, 0.8);`}
}
`}
`;
export const Message = styled.div`
${({ theme }) => css`
${theme.typo["micro-r"]};
color: ${theme.colors["warn"]["40"]};
width: fit-content;
margin: 4px 0px 0px 8px;
flex-shrink: 0;
`}
`;
export const TextareaContainer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
`;
이렇게 만들어진 textarea의 높이를 overriding 할 때는 아래와 같이 calc(원하는 높이 * scale)를 height로 설정해줘야 한다. 살짝 번거롭지만 현재로서는 이게 최선인 것 같다...!
import Textarea from "@/components/TextArea";
export const FormTextarea = styled(Textarea)`
width: calc(100% - 81px);
& > textarea {
height: calc(233px * 1.1429);
border-radius: 12px;
}
`;
이렇게 input과 textarea 모두 화면 확대를 막을 수 있다!
결과물
코딩하는 데 시간은 꽤 걸렸지만 잘 동작하는 걸 보니 아주 뿌듯하다.
+) 간혹 safari 환경에서 화면 상단에 위치한 input을 터치했을 때 확대는 되지 않지만 화면 상단에 빈 공간이 생기는 경우가 있을 수 있는데 이럴 땐 아이폰 설정에서 safari 방문 기록 및 웹 사이트 데이터 지우기를 하면 공간이 없어진다. 이건 또 대체 어떻게 해결해야 하는 건가 막막했는데 간단히 해결되어서 다행이었다..^.^ ios는 여러모로 고려해야 할 게 많아서 항상 날 고생시킨다..
'Develop > React' 카테고리의 다른 글
Zustand 사용해 보기 (+ Redux와 비교) (0) | 2024.06.27 |
---|---|
[React] 라이브러리 없이 Toast 구현하기 (2) | 2023.11.02 |
[React] useReducer (0) | 2023.09.11 |
[React] React Router - RouterProvider, createBrowserRouter (0) | 2023.09.11 |
[React] Pagination 구현하기 (1) | 2023.08.14 |