diff --git a/docs/components/Tooltip.md b/docs/components/Tooltip.md
new file mode 100644
index 0000000..d773bad
--- /dev/null
+++ b/docs/components/Tooltip.md
@@ -0,0 +1,31 @@
+# `[Component] Tooltip`
+
+## `type`
+
+```ts
+export interface TooltipProps {
+ children: React.ReactNode;
+ placement?: Placement;
+ content?: React.ReactNode;
+ offset?: number;
+ color?: NormalColorType;
+}
+```
+
+## `example`
+
+```tsx
+import { Tooltip } from '@wap-ui/react';
+
+const App = () => {
+ return (
+ <>
+
+
+
+ >
+ );
+};
+```
diff --git a/packages/react/package.json b/packages/react/package.json
index e275ff4..1349c5a 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@wap-ui/react",
- "version": "1.4.0",
+ "version": "2.0.0",
"description": "WAP UI / React UI Components Library",
"repository": "https://github.com/pknu-wap/2022_2_WAP_WEB_TEAM1.git",
"author": "neko113 ",
diff --git a/packages/react/src/components/Tooltip/Tooltip.stories.tsx b/packages/react/src/components/Tooltip/Tooltip.stories.tsx
new file mode 100644
index 0000000..ed48829
--- /dev/null
+++ b/packages/react/src/components/Tooltip/Tooltip.stories.tsx
@@ -0,0 +1,103 @@
+import styled from '@emotion/styled';
+import { ComponentStory, ComponentMeta } from '@storybook/react';
+import React from 'react';
+import { Tooltip, TooltipProps } from './Tooltip';
+import { Button } from '../Button';
+
+export default {
+ title: 'Components/Tooltip',
+ component: Tooltip,
+} as ComponentMeta;
+
+const Template: ComponentStory = (args: TooltipProps) => (
+
+
+
+
+
+);
+
+export const Default = Template.bind({});
+
+Default.args = {
+ placement: 'top',
+ content: 'Boooooom!',
+};
+
+export const Placement = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const Color = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const FlexColumn = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ margin-left: 5rem;
+ margin-top: 2rem;
+`;
+
+const Container = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 300px;
+ gap: 5rem;
+ margin-top: 5rem;
+`;
diff --git a/packages/react/src/components/Tooltip/Tooltip.styles.tsx b/packages/react/src/components/Tooltip/Tooltip.styles.tsx
new file mode 100644
index 0000000..c3d9f5b
--- /dev/null
+++ b/packages/react/src/components/Tooltip/Tooltip.styles.tsx
@@ -0,0 +1,71 @@
+import { css } from '@emotion/react';
+import styled from '@emotion/styled';
+import { NormalColorType, palette } from '../../theme';
+import { TooltipIconPlacement, TooltipPlacement } from './placement';
+
+export const StyledTooltipTrigger = styled.div`
+ width: max-content;
+ display: inherit;
+`;
+
+export const StyledTooltipArrow = styled.div<{
+ tooltipIconPlacement: TooltipIconPlacement;
+}>`
+ border-radius: 0 0 2px 0;
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ background-color: #ccc;
+ opacity: 0;
+ transition: 0.1s ease-in-out;
+
+ ${({ tooltipIconPlacement }) => css`
+ top: ${tooltipIconPlacement.top};
+ left: ${tooltipIconPlacement.left};
+ right: ${tooltipIconPlacement.right};
+ bottom: ${tooltipIconPlacement.bottom};
+ transform: ${tooltipIconPlacement.transform};
+ `};
+`;
+
+export const StyledTooltipContent = styled.div<{
+ visible: boolean;
+ color: NormalColorType;
+ tooltipPlacement: TooltipPlacement;
+}>`
+ position: absolute;
+ border-radius: 14px;
+ padding: 8px 12px;
+ opacity: 0;
+ transition: 0.1s ease-in-out;
+
+ ${({ tooltipPlacement }) => css`
+ top: calc(${tooltipPlacement.top} + 6px);
+ left: ${tooltipPlacement.left};
+ transform: ${tooltipPlacement.transform};
+ `};
+
+ ${({ visible, tooltipPlacement }) =>
+ visible &&
+ css`
+ opacity: 1;
+ top: ${tooltipPlacement.top};
+ ${StyledTooltipArrow} {
+ opacity: 1;
+ }
+ `};
+
+ ${({ color }) => css`
+ background-color: ${palette[color]};
+ ${StyledTooltipArrow} {
+ background-color: ${palette[color]};
+ }
+ `};
+`;
+
+export const StyledTooltip = styled.div`
+ position: relative;
+ font-size: 14px;
+ color: #fff;
+ line-height: 1.5;
+`;
diff --git a/packages/react/src/components/Tooltip/Tooltip.tsx b/packages/react/src/components/Tooltip/Tooltip.tsx
new file mode 100644
index 0000000..b54549b
--- /dev/null
+++ b/packages/react/src/components/Tooltip/Tooltip.tsx
@@ -0,0 +1,76 @@
+import { TooltipContent } from './TooltipContent';
+import * as S from './Tooltip.styles';
+import React, { useRef, useState } from 'react';
+import { Placement } from './placement';
+import { NormalColorType } from '../../theme';
+
+export interface TooltipProps {
+ children: React.ReactNode;
+ placement?: Placement;
+ content?: React.ReactNode;
+ offset?: number;
+ color?: NormalColorType;
+}
+
+/**
+ * @example
+ * ```tsx
+ *
+ *
+ *
+ * ```
+ */
+export const Tooltip = ({
+ children,
+ placement = 'top',
+ content,
+ offset,
+ color = 'primary',
+ ...props
+}: TooltipProps) => {
+ const ref = useRef(null);
+ const [visible, setVisible] = useState(false);
+ const timer = useRef();
+
+ const contentProps = {
+ placement,
+ parent: ref,
+ visible,
+ offset,
+ color,
+ };
+
+ const handleChangeVisible = (nextState: boolean) => {
+ const clear = () => {
+ clearTimeout(timer.current);
+ timer.current = undefined;
+ };
+ const handler = (nextState: boolean) => {
+ setVisible(nextState);
+ clear();
+ };
+
+ clear();
+ if (nextState) {
+ timer.current = window.setTimeout(() => handler(true), 100);
+
+ return;
+ }
+ timer.current = window.setTimeout(() => handler(false), 100);
+ };
+
+ return (
+ handleChangeVisible(true)}
+ onMouseLeave={() => handleChangeVisible(false)}
+ {...props}
+ >
+ {children}
+ {content ? (
+ {content}
+ ) : null}
+ {/* null을 사용하지 않으면, TooltipContent가 렌더링되지 않는다. */}
+
+ );
+};
diff --git a/packages/react/src/components/Tooltip/TooltipContent.tsx b/packages/react/src/components/Tooltip/TooltipContent.tsx
new file mode 100644
index 0000000..a79e7dd
--- /dev/null
+++ b/packages/react/src/components/Tooltip/TooltipContent.tsx
@@ -0,0 +1,81 @@
+import React, { MutableRefObject, useEffect, useMemo, useState } from 'react';
+import * as S from './Tooltip.styles';
+import { createPortal } from 'react-dom';
+import usePortal from '../../hooks/usePortal';
+import {
+ Placement,
+ TooltipPlacement,
+ defaultTooltipPlacement,
+ getIconPlacement,
+ getPlacement,
+ getRect,
+} from './placement';
+import { NormalColorType } from '../../theme';
+
+interface TooltipContentProps {
+ placement?: Placement;
+ visible: boolean;
+ children?: React.ReactNode;
+ parent?: MutableRefObject | undefined;
+ offset?: number;
+ color?: NormalColorType;
+}
+
+export const TooltipContent = ({
+ placement = 'top',
+ visible,
+ children,
+ offset = 12,
+ color = 'primary',
+ parent,
+}: TooltipContentProps) => {
+ const el = usePortal('tooltip');
+ const [rect, setRect] = useState(defaultTooltipPlacement);
+
+ if (!parent) return null;
+
+ const updateRect = () => {
+ const pos = getPlacement(placement, getRect(parent), offset);
+
+ setRect(pos);
+ };
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const { transform, top, left, right, bottom } = useMemo(
+ () => getIconPlacement(placement, 5),
+ [placement],
+ );
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ useEffect(() => {
+ updateRect();
+ }, [visible]);
+
+ if (!el) return null;
+
+ return createPortal(
+
+
+
+ {children}
+
+ ,
+ el,
+ );
+};
diff --git a/packages/react/src/components/Tooltip/index.ts b/packages/react/src/components/Tooltip/index.ts
new file mode 100644
index 0000000..b44d466
--- /dev/null
+++ b/packages/react/src/components/Tooltip/index.ts
@@ -0,0 +1 @@
+export { Tooltip } from './Tooltip';
diff --git a/packages/react/src/components/Tooltip/placement.ts b/packages/react/src/components/Tooltip/placement.ts
new file mode 100644
index 0000000..7f4ac45
--- /dev/null
+++ b/packages/react/src/components/Tooltip/placement.ts
@@ -0,0 +1,139 @@
+import { MutableRefObject } from 'react';
+
+export type Placement = 'top' | 'right' | 'bottom' | 'left';
+
+interface ParentDomRect {
+ top: number;
+ left: number;
+ right: number;
+ bottom: number;
+ width: number;
+ height: number;
+}
+
+interface ReactiveDomReact {
+ top: number;
+ bottom: number;
+ left: number;
+ right: number;
+ width: number;
+ height: number;
+}
+
+// defaultRect는 보이지 않는 상태에서의 위치를 잡기 위한 값
+const defaultRect: ReactiveDomReact = {
+ top: -1000,
+ left: -1000,
+ right: -1000,
+ bottom: -1000,
+ width: 0,
+ height: 0,
+};
+
+export interface TooltipPlacement {
+ top: string;
+ left: string;
+ transform: string;
+}
+
+export const defaultTooltipPlacement = {
+ top: '-1000px',
+ left: '-1000px',
+ transform: 'none',
+};
+
+export interface TooltipIconPlacement {
+ top: string;
+ left: string;
+ right: string;
+ bottom: string;
+ transform: string;
+}
+
+export const getRect = (ref: MutableRefObject) => {
+ if (!ref || !ref.current) return defaultRect;
+
+ const rect = ref.current.getBoundingClientRect();
+
+ return {
+ ...rect,
+ width: rect.width || rect.right - rect.left,
+ height: rect.height || rect.bottom - rect.top,
+ top: rect.top + document.documentElement.scrollTop,
+ bottom: rect.bottom + document.documentElement.scrollTop,
+ left: rect.left + document.documentElement.scrollLeft,
+ right: rect.right + document.documentElement.scrollLeft,
+ };
+};
+
+export const getPlacement = (
+ placement: Placement,
+ rect: ParentDomRect,
+ offset: number,
+) => {
+ const placements = {
+ top: {
+ top: `${rect.top - offset}px`,
+ left: `${rect.left + rect.width / 2}px`,
+ transform: 'translate(-50%, -100%)',
+ },
+ bottom: {
+ top: `${rect.bottom + offset}px`,
+ left: `${rect.left + rect.width / 2}px`,
+ transform: 'translate(-50%, 0)',
+ },
+ left: {
+ top: `${rect.top + rect.height / 2}px`,
+ left: `${rect.left - offset}px`,
+ transform: 'translate(-100%, -50%)',
+ },
+ right: {
+ top: `${rect.top + rect.height / 2}px`,
+ left: `${rect.right + offset}px`,
+ transform: 'translate(0, -50%)',
+ },
+ };
+
+ return placements[placement];
+};
+
+export const getIconPlacement = (
+ placement: Placement,
+ offset: number,
+): TooltipIconPlacement => {
+ const placements = {
+ top: {
+ top: 'auto',
+ right: 'auto',
+ left: '50%',
+ bottom: '0px',
+ transform: 'translate(-50%, 100%) rotate(45deg)',
+ },
+
+ bottom: {
+ top: '0px',
+ right: 'auto',
+ left: '50%',
+ bottom: 'auto',
+ transform: 'translate(-50%, -100%) rotate(225deg)',
+ },
+
+ left: {
+ top: '50%',
+ right: `-${offset - 1}px`,
+ left: 'auto',
+ bottom: 'auto',
+ transform: 'translate(100%, -50%) rotate(-45deg)',
+ },
+
+ right: {
+ top: '50%',
+ right: 'auto',
+ left: `-${offset - 1}px`,
+ bottom: 'auto',
+ transform: 'translate(-100%, -50%) rotate(135deg)',
+ },
+ };
+
+ return placements[placement];
+};
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index 7e5f3f5..f52fff4 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -10,3 +10,4 @@ export * from './Loader';
export * from './Toast';
export * from './Dropdown';
export * from './ScrollToTop';
+export * from './Tooltip';